mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-11 04:09:40 -06:00
Merge branch 'develop' into enhancement/check-if-url-resolves
This commit is contained in:
28
.prettierrc
28
.prettierrc
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"printWidth": 90,
|
||||
"useTabs": true,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": false,
|
||||
"bracketSpacing": true,
|
||||
"proseWrap": "preserve",
|
||||
"bracketSameLine": false,
|
||||
"singleAttributePerLine": true,
|
||||
"semi": true,
|
||||
"jsx-single-quote": false,
|
||||
"quoteProps": "as-needed",
|
||||
"arrowParens": "always",
|
||||
"trailingComma": "es5",
|
||||
"htmlWhitespaceSensitivity": "css"
|
||||
"printWidth": 90,
|
||||
"useTabs": true,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": false,
|
||||
"bracketSpacing": true,
|
||||
"proseWrap": "preserve",
|
||||
"bracketSameLine": false,
|
||||
"singleAttributePerLine": true,
|
||||
"semi": true,
|
||||
"jsxSingleQuote": false,
|
||||
"quoteProps": "as-needed",
|
||||
"arrowParens": "always",
|
||||
"trailingComma": "es5",
|
||||
"htmlWhitespaceSensitivity": "css"
|
||||
}
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react/jsx-runtime',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||
settings: { react: { version: '18.2' } },
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react/jsx-no-target-blank': 'off',
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react/jsx-runtime",
|
||||
"plugin:react-hooks/recommended",
|
||||
],
|
||||
ignorePatterns: ["dist", ".eslintrc.cjs"],
|
||||
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
|
||||
settings: { react: { version: "18.2" } },
|
||||
plugins: ["react-refresh"],
|
||||
rules: {
|
||||
"react/jsx-no-target-blank": "off",
|
||||
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<!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>
|
||||
<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>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script
|
||||
type="module"
|
||||
src="/src/main.jsx"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
13002
Client/package-lock.json
generated
13002
Client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,49 +1,49 @@
|
||||
{
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.5",
|
||||
"@fontsource/roboto": "^5.0.13",
|
||||
"@mui/icons-material": "^5.15.17",
|
||||
"@mui/lab": "^5.0.0-alpha.170",
|
||||
"@mui/material": "^5.15.16",
|
||||
"@mui/x-charts": "^7.5.1",
|
||||
"@mui/x-data-grid": "7.3.2",
|
||||
"@mui/x-date-pickers": "7.3.2",
|
||||
"@reduxjs/toolkit": "2.2.5",
|
||||
"axios": "^1.7.4",
|
||||
"chart.js": "^4.4.3",
|
||||
"dayjs": "1.11.11",
|
||||
"joi": "17.13.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "9.1.2",
|
||||
"react-router": "^6.23.0",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"react-toastify": "^10.0.5",
|
||||
"recharts": "2.13.0-alpha.4",
|
||||
"redux-persist": "6.0.0",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"prettier": "^3.3.3",
|
||||
"vite": "^5.2.0"
|
||||
}
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.5",
|
||||
"@fontsource/roboto": "^5.0.13",
|
||||
"@mui/icons-material": "^5.15.17",
|
||||
"@mui/lab": "^5.0.0-alpha.170",
|
||||
"@mui/material": "^5.15.16",
|
||||
"@mui/x-charts": "^7.5.1",
|
||||
"@mui/x-data-grid": "7.3.2",
|
||||
"@mui/x-date-pickers": "7.3.2",
|
||||
"@reduxjs/toolkit": "2.2.5",
|
||||
"axios": "^1.7.4",
|
||||
"chart.js": "^4.4.3",
|
||||
"dayjs": "1.11.11",
|
||||
"joi": "17.13.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "9.1.2",
|
||||
"react-router": "^6.23.0",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"react-toastify": "^10.0.5",
|
||||
"recharts": "2.13.0-alpha.4",
|
||||
"redux-persist": "6.0.0",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"prettier": "^3.3.3",
|
||||
"vite": "^5.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,177 +40,177 @@ import { getAppSettings } from "./Features/Settings/settingsSlice";
|
||||
import { logger } from "./Utils/Logger"; // Import the logger
|
||||
import { networkService } from "./main";
|
||||
function App() {
|
||||
const AdminCheckedRegister = withAdminCheck(Register);
|
||||
const MonitorsWithAdminProp = withAdminProp(Monitors);
|
||||
const MonitorDetailsWithAdminProp = withAdminProp(Details);
|
||||
const PageSpeedWithAdminProp = withAdminProp(PageSpeed);
|
||||
const PageSpeedDetailsWithAdminProp = withAdminProp(PageSpeedDetails);
|
||||
const MaintenanceWithAdminProp = withAdminProp(Maintenance);
|
||||
const SettingsWithAdminProp = withAdminProp(Settings);
|
||||
const AdvancedSettingsWithAdminProp = withAdminProp(AdvancedSettings);
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const dispatch = useDispatch();
|
||||
const AdminCheckedRegister = withAdminCheck(Register);
|
||||
const MonitorsWithAdminProp = withAdminProp(Monitors);
|
||||
const MonitorDetailsWithAdminProp = withAdminProp(Details);
|
||||
const PageSpeedWithAdminProp = withAdminProp(PageSpeed);
|
||||
const PageSpeedDetailsWithAdminProp = withAdminProp(PageSpeedDetails);
|
||||
const MaintenanceWithAdminProp = withAdminProp(Maintenance);
|
||||
const SettingsWithAdminProp = withAdminProp(Settings);
|
||||
const AdvancedSettingsWithAdminProp = withAdminProp(AdvancedSettings);
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (authToken) {
|
||||
dispatch(getAppSettings({ authToken }));
|
||||
}
|
||||
}, [dispatch, authToken]);
|
||||
useEffect(() => {
|
||||
if (authToken) {
|
||||
dispatch(getAppSettings({ authToken }));
|
||||
}
|
||||
}, [dispatch, authToken]);
|
||||
|
||||
// Cleanup
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
logger.cleanup();
|
||||
networkService.cleanup();
|
||||
};
|
||||
}, []);
|
||||
// Cleanup
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
logger.cleanup();
|
||||
networkService.cleanup();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
|
||||
<CssBaseline />
|
||||
<Routes>
|
||||
<Route
|
||||
exact
|
||||
path="/"
|
||||
element={<HomeLayout />}
|
||||
>
|
||||
<Route
|
||||
exact
|
||||
path="/"
|
||||
element={<ProtectedRoute Component={MonitorsWithAdminProp} />}
|
||||
/>
|
||||
<Route
|
||||
path="/monitors"
|
||||
element={<ProtectedRoute Component={MonitorsWithAdminProp} />}
|
||||
/>
|
||||
<Route
|
||||
path="/monitors/create/:monitorId?"
|
||||
element={<ProtectedRoute Component={CreateMonitor} />}
|
||||
/>
|
||||
<Route
|
||||
path="/monitors/:monitorId/"
|
||||
element={<ProtectedRoute Component={MonitorDetailsWithAdminProp} />}
|
||||
/>
|
||||
<Route
|
||||
path="/monitors/configure/:monitorId/"
|
||||
element={<ProtectedRoute Component={Configure} />}
|
||||
/>
|
||||
<Route
|
||||
path="incidents/:monitorId?"
|
||||
element={<ProtectedRoute Component={Incidents} />}
|
||||
/>
|
||||
return (
|
||||
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
|
||||
<CssBaseline />
|
||||
<Routes>
|
||||
<Route
|
||||
exact
|
||||
path="/"
|
||||
element={<HomeLayout />}
|
||||
>
|
||||
<Route
|
||||
exact
|
||||
path="/"
|
||||
element={<ProtectedRoute Component={MonitorsWithAdminProp} />}
|
||||
/>
|
||||
<Route
|
||||
path="/monitors"
|
||||
element={<ProtectedRoute Component={MonitorsWithAdminProp} />}
|
||||
/>
|
||||
<Route
|
||||
path="/monitors/create/:monitorId?"
|
||||
element={<ProtectedRoute Component={CreateMonitor} />}
|
||||
/>
|
||||
<Route
|
||||
path="/monitors/:monitorId/"
|
||||
element={<ProtectedRoute Component={MonitorDetailsWithAdminProp} />}
|
||||
/>
|
||||
<Route
|
||||
path="/monitors/configure/:monitorId/"
|
||||
element={<ProtectedRoute Component={Configure} />}
|
||||
/>
|
||||
<Route
|
||||
path="incidents/:monitorId?"
|
||||
element={<ProtectedRoute Component={Incidents} />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="status"
|
||||
element={<ProtectedRoute Component={Status} />}
|
||||
/>
|
||||
<Route
|
||||
path="integrations"
|
||||
element={<ProtectedRoute Component={Integrations} />}
|
||||
/>
|
||||
<Route
|
||||
path="maintenance"
|
||||
element={<ProtectedRoute Component={MaintenanceWithAdminProp} />}
|
||||
/>
|
||||
<Route
|
||||
path="/maintenance/create/:maintenanceWindowId?"
|
||||
element={<CreateNewMaintenanceWindow />}
|
||||
/>
|
||||
<Route
|
||||
path="settings"
|
||||
element={<ProtectedRoute Component={SettingsWithAdminProp} />}
|
||||
/>
|
||||
<Route
|
||||
path="advanced-settings"
|
||||
element={<ProtectedRoute Component={AdvancedSettingsWithAdminProp} />}
|
||||
/>
|
||||
<Route
|
||||
path="account/profile"
|
||||
element={
|
||||
<ProtectedRoute
|
||||
Component={Account}
|
||||
open="profile"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="account/password"
|
||||
element={
|
||||
<ProtectedRoute
|
||||
Component={Account}
|
||||
open="password"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="account/team"
|
||||
element={
|
||||
<ProtectedRoute
|
||||
Component={Account}
|
||||
open="team"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<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
|
||||
path="status"
|
||||
element={<ProtectedRoute Component={Status} />}
|
||||
/>
|
||||
<Route
|
||||
path="integrations"
|
||||
element={<ProtectedRoute Component={Integrations} />}
|
||||
/>
|
||||
<Route
|
||||
path="maintenance"
|
||||
element={<ProtectedRoute Component={MaintenanceWithAdminProp} />}
|
||||
/>
|
||||
<Route
|
||||
path="/maintenance/create/:maintenanceWindowId?"
|
||||
element={<CreateNewMaintenanceWindow />}
|
||||
/>
|
||||
<Route
|
||||
path="settings"
|
||||
element={<ProtectedRoute Component={SettingsWithAdminProp} />}
|
||||
/>
|
||||
<Route
|
||||
path="advanced-settings"
|
||||
element={<ProtectedRoute Component={AdvancedSettingsWithAdminProp} />}
|
||||
/>
|
||||
<Route
|
||||
path="account/profile"
|
||||
element={
|
||||
<ProtectedRoute
|
||||
Component={Account}
|
||||
open="profile"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="account/password"
|
||||
element={
|
||||
<ProtectedRoute
|
||||
Component={Account}
|
||||
open="password"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="account/team"
|
||||
element={
|
||||
<ProtectedRoute
|
||||
Component={Account}
|
||||
open="team"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<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
|
||||
exact
|
||||
path="/login"
|
||||
element={<Login />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/login"
|
||||
element={<Login />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
exact
|
||||
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 />}
|
||||
/>
|
||||
<Route
|
||||
path="/check-email"
|
||||
element={<CheckEmail />}
|
||||
/>
|
||||
<Route
|
||||
path="/set-new-password/:token"
|
||||
element={<SetNewPassword />}
|
||||
/>
|
||||
<Route
|
||||
path="/new-password-confirmed"
|
||||
element={<NewPasswordConfirmed />}
|
||||
/>
|
||||
</Routes>
|
||||
<ToastContainer />
|
||||
</ThemeProvider>
|
||||
);
|
||||
<Route
|
||||
exact
|
||||
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 />}
|
||||
/>
|
||||
<Route
|
||||
path="/check-email"
|
||||
element={<CheckEmail />}
|
||||
/>
|
||||
<Route
|
||||
path="/set-new-password/:token"
|
||||
element={<SetNewPassword />}
|
||||
/>
|
||||
<Route
|
||||
path="/new-password-confirmed"
|
||||
element={<NewPasswordConfirmed />}
|
||||
/>
|
||||
</Routes>
|
||||
<ToastContainer />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
.alert {
|
||||
margin: 0;
|
||||
width: fit-content;
|
||||
margin: 0;
|
||||
width: fit-content;
|
||||
}
|
||||
.alert,
|
||||
.alert button,
|
||||
.alert .MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ import "./index.css";
|
||||
*/
|
||||
|
||||
const icons = {
|
||||
info: <InfoOutlinedIcon />,
|
||||
error: <ErrorOutlineOutlinedIcon />,
|
||||
warning: <WarningAmberOutlinedIcon />,
|
||||
info: <InfoOutlinedIcon />,
|
||||
error: <ErrorOutlineOutlinedIcon />,
|
||||
warning: <WarningAmberOutlinedIcon />,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -30,94 +30,92 @@ const icons = {
|
||||
*/
|
||||
|
||||
const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => {
|
||||
const theme = useTheme();
|
||||
const { text, bg, border } = theme.palette[variant];
|
||||
const icon = icons[variant];
|
||||
const theme = useTheme();
|
||||
const { text, bg, border } = theme.palette[variant];
|
||||
const icon = icons[variant];
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems={hasIcon ? "" : "center"}
|
||||
className="alert row-stack"
|
||||
gap={theme.spacing(8)}
|
||||
sx={{
|
||||
padding: hasIcon
|
||||
? theme.spacing(8)
|
||||
: `${theme.spacing(4)} ${theme.spacing(8)}`,
|
||||
backgroundColor: bg,
|
||||
border: `solid 1px ${border}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
}}
|
||||
>
|
||||
{hasIcon && <Box sx={{ color: text }}>{icon}</Box>}
|
||||
<Stack direction="column" gap="2px" sx={{ flex: 1 }}>
|
||||
{title && (
|
||||
<Typography sx={{ fontWeight: "700", color: `${text}` }}>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
{body && (
|
||||
<Typography sx={{ fontWeight: "400", color: `${text}` }}>
|
||||
{body}
|
||||
</Typography>
|
||||
)}
|
||||
{hasIcon && isToast && (
|
||||
<Button
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
fontWeight: "600",
|
||||
width: "fit-content",
|
||||
mt: theme.spacing(4),
|
||||
padding: 0,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
{isToast && (
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
alignSelf: "flex-start",
|
||||
ml: "auto",
|
||||
mr: "-5px",
|
||||
mt: hasIcon ? "-5px" : 0,
|
||||
padding: "5px",
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CloseIcon
|
||||
sx={{
|
||||
fontSize: "20px",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems={hasIcon ? "" : "center"}
|
||||
className="alert row-stack"
|
||||
gap={theme.spacing(8)}
|
||||
sx={{
|
||||
padding: hasIcon ? theme.spacing(8) : `${theme.spacing(4)} ${theme.spacing(8)}`,
|
||||
backgroundColor: bg,
|
||||
border: `solid 1px ${border}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
}}
|
||||
>
|
||||
{hasIcon && <Box sx={{ color: text }}>{icon}</Box>}
|
||||
<Stack
|
||||
direction="column"
|
||||
gap="2px"
|
||||
sx={{ flex: 1 }}
|
||||
>
|
||||
{title && (
|
||||
<Typography sx={{ fontWeight: "700", color: `${text}` }}>{title}</Typography>
|
||||
)}
|
||||
{body && (
|
||||
<Typography sx={{ fontWeight: "400", color: `${text}` }}>{body}</Typography>
|
||||
)}
|
||||
{hasIcon && isToast && (
|
||||
<Button
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
fontWeight: "600",
|
||||
width: "fit-content",
|
||||
mt: theme.spacing(4),
|
||||
padding: 0,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
{isToast && (
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
alignSelf: "flex-start",
|
||||
ml: "auto",
|
||||
mr: "-5px",
|
||||
mt: hasIcon ? "-5px" : 0,
|
||||
padding: "5px",
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CloseIcon
|
||||
sx={{
|
||||
fontSize: "20px",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Alert.propTypes = {
|
||||
variant: PropTypes.oneOf(["info", "error", "warning"]).isRequired,
|
||||
title: PropTypes.string,
|
||||
body: PropTypes.string,
|
||||
isToast: PropTypes.bool,
|
||||
hasIcon: PropTypes.bool,
|
||||
onClick: function (props, propName, componentName) {
|
||||
if (props.isToast && !props[propName]) {
|
||||
return new Error(
|
||||
`Prop '${propName}' is required when 'isToast' is true in '${componentName}'.`
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
variant: PropTypes.oneOf(["info", "error", "warning"]).isRequired,
|
||||
title: PropTypes.string,
|
||||
body: PropTypes.string,
|
||||
isToast: PropTypes.bool,
|
||||
hasIcon: PropTypes.bool,
|
||||
onClick: function (props, propName, componentName) {
|
||||
if (props.isToast && !props[propName]) {
|
||||
return new Error(
|
||||
`Prop '${propName}' is required when 'isToast' is true in '${componentName}'.`
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
export default Alert;
|
||||
|
||||
@@ -15,48 +15,48 @@ import { Box, Stack } from "@mui/material";
|
||||
*/
|
||||
|
||||
const PulseDot = ({ color }) => {
|
||||
return (
|
||||
<Stack
|
||||
width="26px"
|
||||
height="24px"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Box
|
||||
minWidth="18px"
|
||||
minHeight="18px"
|
||||
sx={{
|
||||
position: "relative",
|
||||
backgroundColor: color,
|
||||
borderRadius: "50%",
|
||||
"&::before": {
|
||||
content: `""`,
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: "inherit",
|
||||
borderRadius: "50%",
|
||||
animation: "ripple 1.8s ease-out infinite",
|
||||
},
|
||||
"&::after": {
|
||||
content: `""`,
|
||||
position: "absolute",
|
||||
width: "7px",
|
||||
height: "7px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "white",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
width="26px"
|
||||
height="24px"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Box
|
||||
minWidth="18px"
|
||||
minHeight="18px"
|
||||
sx={{
|
||||
position: "relative",
|
||||
backgroundColor: color,
|
||||
borderRadius: "50%",
|
||||
"&::before": {
|
||||
content: `""`,
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: "inherit",
|
||||
borderRadius: "50%",
|
||||
animation: "ripple 1.8s ease-out infinite",
|
||||
},
|
||||
"&::after": {
|
||||
content: `""`,
|
||||
position: "absolute",
|
||||
width: "7px",
|
||||
height: "7px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "white",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
PulseDot.propTypes = {
|
||||
color: PropTypes.string.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default PulseDot;
|
||||
|
||||
@@ -9,19 +9,19 @@ import { useEffect, useState } from "react";
|
||||
* @returns {string}
|
||||
*/
|
||||
const stringToColor = (string) => {
|
||||
let hash = 0;
|
||||
let i;
|
||||
for (i = 0; i < string.length; i += 1) {
|
||||
hash = string.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
let hash = 0;
|
||||
let i;
|
||||
for (i = 0; i < string.length; i += 1) {
|
||||
hash = string.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
|
||||
let color = "#";
|
||||
for (i = 0; i < 3; i += 1) {
|
||||
const value = (hash >> (i * 8)) & 0xff;
|
||||
color += `00${value.toString(16)}`.slice(-2);
|
||||
}
|
||||
let color = "#";
|
||||
for (i = 0; i < 3; i += 1) {
|
||||
const value = (hash >> (i * 8)) & 0xff;
|
||||
color += `00${value.toString(16)}`.slice(-2);
|
||||
}
|
||||
|
||||
return color;
|
||||
return color;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -37,54 +37,52 @@ const stringToColor = (string) => {
|
||||
*/
|
||||
|
||||
const Avatar = ({ src, small, sx }) => {
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
|
||||
const style = small ? { width: 32, height: 32 } : { width: 64, height: 64 };
|
||||
const border = small ? 1 : 3;
|
||||
const style = small ? { width: 32, height: 32 } : { width: 64, height: 64 };
|
||||
const border = small ? 1 : 3;
|
||||
|
||||
const [image, setImage] = useState();
|
||||
useEffect(() => {
|
||||
if (user.avatarImage) {
|
||||
setImage(`data:image/png;base64,${user.avatarImage}`);
|
||||
}
|
||||
}, [user?.avatarImage]);
|
||||
const [image, setImage] = useState();
|
||||
useEffect(() => {
|
||||
if (user.avatarImage) {
|
||||
setImage(`data:image/png;base64,${user.avatarImage}`);
|
||||
}
|
||||
}, [user?.avatarImage]);
|
||||
|
||||
return (
|
||||
<MuiAvatar
|
||||
alt={`${user?.firstName} ${user?.lastName}`}
|
||||
src={
|
||||
src ? src : user?.avatarImage ? image : "/static/images/avatar/2.jpg"
|
||||
}
|
||||
sx={{
|
||||
fontSize: small ? "16px" : "22px",
|
||||
color: "white",
|
||||
fontWeight: 400,
|
||||
backgroundColor: stringToColor(`${user?.firstName} ${user?.lastName}`),
|
||||
display: "inline-flex",
|
||||
"&::before": {
|
||||
content: `""`,
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: `100%`,
|
||||
height: `100%`,
|
||||
border: `${border}px solid rgba(255,255,255,0.2)`,
|
||||
borderRadius: "50%",
|
||||
},
|
||||
...style,
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{user.firstName?.charAt(0)}
|
||||
{user.lastName?.charAt(0)}
|
||||
</MuiAvatar>
|
||||
);
|
||||
return (
|
||||
<MuiAvatar
|
||||
alt={`${user?.firstName} ${user?.lastName}`}
|
||||
src={src ? src : user?.avatarImage ? image : "/static/images/avatar/2.jpg"}
|
||||
sx={{
|
||||
fontSize: small ? "16px" : "22px",
|
||||
color: "white",
|
||||
fontWeight: 400,
|
||||
backgroundColor: stringToColor(`${user?.firstName} ${user?.lastName}`),
|
||||
display: "inline-flex",
|
||||
"&::before": {
|
||||
content: `""`,
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: `100%`,
|
||||
height: `100%`,
|
||||
border: `${border}px solid rgba(255,255,255,0.2)`,
|
||||
borderRadius: "50%",
|
||||
},
|
||||
...style,
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{user.firstName?.charAt(0)}
|
||||
{user.lastName?.charAt(0)}
|
||||
</MuiAvatar>
|
||||
);
|
||||
};
|
||||
|
||||
Avatar.propTypes = {
|
||||
src: PropTypes.string,
|
||||
small: PropTypes.bool,
|
||||
sx: PropTypes.object,
|
||||
src: PropTypes.string,
|
||||
small: PropTypes.bool,
|
||||
sx: PropTypes.object,
|
||||
};
|
||||
|
||||
export default Avatar;
|
||||
|
||||
@@ -1,133 +1,130 @@
|
||||
.MuiTable-root .host {
|
||||
width: fit-content;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: fit-content;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.MuiTable-root .host span {
|
||||
font-size: 11px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.MuiTable-root .label {
|
||||
line-height: 1;
|
||||
border-radius: var(--env-var-radius-2);
|
||||
padding: 7px;
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
line-height: 1;
|
||||
border-radius: var(--env-var-radius-2);
|
||||
padding: 7px;
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
}
|
||||
|
||||
.MuiPaper-root:has(table.MuiTable-root) {
|
||||
box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.MuiTable-root
|
||||
.MuiTableBody-root
|
||||
.MuiTableRow-root:last-child
|
||||
.MuiTableCell-root {
|
||||
border: none;
|
||||
.MuiTable-root .MuiTableBody-root .MuiTableRow-root:last-child .MuiTableCell-root {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.MuiTable-root .MuiTableHead-root .MuiTableCell-root,
|
||||
.MuiTable-root .MuiTableBody-root .MuiTableCell-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
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);
|
||||
font-weight: 500;
|
||||
padding: var(--env-var-spacing-1) var(--env-var-spacing-2);
|
||||
font-weight: 500;
|
||||
}
|
||||
.MuiTable-root .MuiTableHead-root span {
|
||||
display: inline-block;
|
||||
height: 17px;
|
||||
width: 20px;
|
||||
overflow: hidden;
|
||||
margin-bottom: -3px;
|
||||
margin-left: 3px;
|
||||
display: inline-block;
|
||||
height: 17px;
|
||||
width: 20px;
|
||||
overflow: hidden;
|
||||
margin-bottom: -3px;
|
||||
margin-left: 3px;
|
||||
}
|
||||
.MuiTable-root .MuiTableHead-root span svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.MuiTable-root .MuiTableBody-root .MuiTableCell-root {
|
||||
padding: 6px var(--env-var-spacing-2);
|
||||
padding: 6px var(--env-var-spacing-2);
|
||||
}
|
||||
.MuiTable-root .MuiTableBody-root .MuiTableRow-root {
|
||||
height: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.MuiPaper-root + .MuiPagination-root {
|
||||
border-radius: var(--env-var-radius-1);
|
||||
padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2);
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul {
|
||||
justify-content: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root button {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
font-weight: 500;
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
font-weight: 500;
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul li:first-child {
|
||||
margin-right: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul li:last-child {
|
||||
margin-left: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul li:first-child button {
|
||||
padding: 0 var(--env-var-spacing-1) 0 var(--env-var-spacing-1-plus);
|
||||
padding: 0 var(--env-var-spacing-1) 0 var(--env-var-spacing-1-plus);
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul li:last-child button {
|
||||
padding: 0 var(--env-var-spacing-1-plus) 0 var(--env-var-spacing-1);
|
||||
padding: 0 var(--env-var-spacing-1-plus) 0 var(--env-var-spacing-1);
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul li:first-child button::after,
|
||||
.MuiPaper-root + .MuiPagination-root ul li:last-child button::before {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul li:first-child button::after {
|
||||
content: "Previous";
|
||||
margin-left: 15px;
|
||||
content: "Previous";
|
||||
margin-left: 15px;
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root ul li:last-child button::before {
|
||||
content: "Next";
|
||||
margin-right: 15px;
|
||||
content: "Next";
|
||||
margin-right: 15px;
|
||||
}
|
||||
.MuiPaper-root + .MuiPagination-root div.MuiPaginationItem-root {
|
||||
user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.MuiTablePagination-root p {
|
||||
font-weight: 500;
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
font-weight: 500;
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
}
|
||||
.MuiTablePagination-root .MuiTablePagination-select.MuiSelect-select {
|
||||
text-align: left;
|
||||
text-align-last: left;
|
||||
text-align: left;
|
||||
text-align-last: left;
|
||||
}
|
||||
.MuiTablePagination-root button {
|
||||
min-width: 0;
|
||||
padding: 4px;
|
||||
margin-left: 5px;
|
||||
min-width: 0;
|
||||
padding: 4px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.MuiTablePagination-root svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
.MuiTablePagination-root .MuiSelect-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: 50%;
|
||||
right: 8%;
|
||||
transform: translateY(-50%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: 50%;
|
||||
right: 8%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.MuiTablePagination-root button.Mui-disabled {
|
||||
opacity: 0.4;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.table-container .MuiTable-root .MuiTableHead-root .MuiTableCell-root {
|
||||
text-transform: uppercase;
|
||||
opacity: 0.8;
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.8;
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
font-weight: 400;
|
||||
}
|
||||
.monitors .MuiTableCell-root:not(:first-of-type):not(:last-of-type),
|
||||
.monitors .MuiTableCell-root:not(:first-of-type):not(:last-of-type) {
|
||||
padding-left: var(--env-var-spacing-1);
|
||||
padding-right: var(--env-var-spacing-1);
|
||||
padding-left: var(--env-var-spacing-1);
|
||||
padding-right: var(--env-var-spacing-1);
|
||||
}
|
||||
|
||||
@@ -2,18 +2,18 @@ import PropTypes from "prop-types";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import {
|
||||
TableContainer,
|
||||
Paper,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
TablePagination,
|
||||
Box,
|
||||
Typography,
|
||||
Stack,
|
||||
Button,
|
||||
TableContainer,
|
||||
Paper,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
TablePagination,
|
||||
Box,
|
||||
Typography,
|
||||
Stack,
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setRowsPerPage } from "../../Features/UI/uiSlice";
|
||||
@@ -36,64 +36,64 @@ import "./index.css";
|
||||
* @returns {JSX.Element} Pagination actions component.
|
||||
*/
|
||||
const TablePaginationActions = (props) => {
|
||||
const { count, page, rowsPerPage, onPageChange } = 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));
|
||||
};
|
||||
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>
|
||||
);
|
||||
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,
|
||||
count: PropTypes.number.isRequired,
|
||||
page: PropTypes.number.isRequired,
|
||||
rowsPerPage: PropTypes.number.isRequired,
|
||||
onPageChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -150,172 +150,173 @@ TablePaginationActions.propTypes = {
|
||||
*/
|
||||
|
||||
const BasicTable = ({ data, paginated, reversed, table }) => {
|
||||
const DEFAULT_ROWS_PER_PAGE = 5;
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const uiState = useSelector((state) => state.ui);
|
||||
let rowsPerPage = uiState?.[table]?.rowsPerPage ?? DEFAULT_ROWS_PER_PAGE;
|
||||
const [page, setPage] = useState(0);
|
||||
const DEFAULT_ROWS_PER_PAGE = 5;
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const uiState = useSelector((state) => state.ui);
|
||||
let rowsPerPage = uiState?.[table]?.rowsPerPage ?? DEFAULT_ROWS_PER_PAGE;
|
||||
const [page, setPage] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(0);
|
||||
}, [data]);
|
||||
useEffect(() => {
|
||||
setPage(0);
|
||||
}, [data]);
|
||||
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
dispatch(
|
||||
setRowsPerPage({
|
||||
value: parseInt(event.target.value, 10),
|
||||
table: table,
|
||||
})
|
||||
);
|
||||
setPage(0);
|
||||
};
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
dispatch(
|
||||
setRowsPerPage({
|
||||
value: parseInt(event.target.value, 10),
|
||||
table: table,
|
||||
})
|
||||
);
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
let displayData = [];
|
||||
let displayData = [];
|
||||
|
||||
if (data && data.rows) {
|
||||
let rows = reversed ? [...data.rows].reverse() : data.rows;
|
||||
displayData = paginated
|
||||
? rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||
: rows;
|
||||
}
|
||||
if (data && data.rows) {
|
||||
let rows = reversed ? [...data.rows].reverse() : data.rows;
|
||||
displayData = paginated
|
||||
? rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||
: rows;
|
||||
}
|
||||
|
||||
if (!data || !data.cols || !data.rows) {
|
||||
return <div>No data</div>;
|
||||
}
|
||||
if (!data || !data.cols || !data.rows) {
|
||||
return <div>No data</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, data.rows.length);
|
||||
return `${start} - ${end}`;
|
||||
};
|
||||
/**
|
||||
* 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, data.rows.length);
|
||||
return `${start} - ${end}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{data.cols.map((col) => (
|
||||
<TableCell key={col.id}>{col.name}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{displayData.map((row) => {
|
||||
return (
|
||||
<TableRow
|
||||
sx={{
|
||||
cursor: row.handleClick ? "pointer" : "default",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
}}
|
||||
key={row.id}
|
||||
onClick={row.handleClick ? row.handleClick : null}
|
||||
>
|
||||
{row.data.map((cell) => {
|
||||
return <TableCell key={cell.id}>{cell.data}</TableCell>;
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{paginated && (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
px={theme.spacing(4)}
|
||||
sx={{
|
||||
"& p": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography px={theme.spacing(2)} fontSize={12} sx={{ opacity: 0.7 }}>
|
||||
Showing {getRange()} of {data.rows.length} monitor(s)
|
||||
</Typography>
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={data.rows.length}
|
||||
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,
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{data.cols.map((col) => (
|
||||
<TableCell key={col.id}>{col.name}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{displayData.map((row) => {
|
||||
return (
|
||||
<TableRow
|
||||
sx={{
|
||||
cursor: row.handleClick ? "pointer" : "default",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
}}
|
||||
key={row.id}
|
||||
onClick={row.handleClick ? row.handleClick : null}
|
||||
>
|
||||
{row.data.map((cell) => {
|
||||
return <TableCell key={cell.id}>{cell.data}</TableCell>;
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{paginated && (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
px={theme.spacing(4)}
|
||||
sx={{
|
||||
"& p": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
px={theme.spacing(2)}
|
||||
fontSize={12}
|
||||
sx={{ opacity: 0.7 }}
|
||||
>
|
||||
Showing {getRange()} of {data.rows.length} monitor(s)
|
||||
</Typography>
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={data.rows.length}
|
||||
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,
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
BasicTable.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
paginated: PropTypes.bool,
|
||||
reversed: PropTypes.bool,
|
||||
rowPerPage: PropTypes.number,
|
||||
table: PropTypes.string,
|
||||
data: PropTypes.object.isRequired,
|
||||
paginated: PropTypes.bool,
|
||||
reversed: PropTypes.bool,
|
||||
rowPerPage: PropTypes.number,
|
||||
table: PropTypes.string,
|
||||
};
|
||||
|
||||
export default BasicTable;
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
.MuiBreadcrumbs-root {
|
||||
height: 34px;
|
||||
height: 34px;
|
||||
}
|
||||
.MuiBreadcrumbs-root svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.MuiBreadcrumbs-root .MuiBreadcrumbs-li a {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
font-weight: 400;
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
font-weight: 400;
|
||||
}
|
||||
.MuiBreadcrumbs-root .MuiBreadcrumbs-li:not(:last-child) {
|
||||
cursor: pointer;
|
||||
cursor: pointer;
|
||||
}
|
||||
.MuiBreadcrumbs-root .MuiBreadcrumbs-li:last-child a {
|
||||
font-weight: 500;
|
||||
opacity: 1;
|
||||
cursor: default;
|
||||
font-weight: 500;
|
||||
opacity: 1;
|
||||
cursor: default;
|
||||
}
|
||||
.MuiBreadcrumbs-root .MuiBreadcrumbs-separator {
|
||||
margin: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -18,59 +18,59 @@ import "./index.css";
|
||||
*/
|
||||
|
||||
const Breadcrumbs = ({ list }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<MUIBreadcrumbs
|
||||
separator={<ArrowRight />}
|
||||
aria-label="breadcrumb"
|
||||
px={theme.spacing(2)}
|
||||
py={theme.spacing(3.5)}
|
||||
width="fit-content"
|
||||
backgroundColor={theme.palette.background.fill}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
lineHeight="18px"
|
||||
sx={{
|
||||
"& .MuiBreadcrumbs-li:not(:last-of-type):hover a": {
|
||||
backgroundColor: theme.palette.other.fill,
|
||||
opacity: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{list.map((item, index) => {
|
||||
return (
|
||||
<Box
|
||||
component="a"
|
||||
key={`${item.name}-${index}`}
|
||||
px={theme.spacing(4)}
|
||||
pt={theme.spacing(2)}
|
||||
pb={theme.spacing(3)}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
onClick={() => navigate(item.path)}
|
||||
sx={{
|
||||
opacity: 0.8,
|
||||
textTransform: "capitalize",
|
||||
"&, &:hover": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</MUIBreadcrumbs>
|
||||
);
|
||||
return (
|
||||
<MUIBreadcrumbs
|
||||
separator={<ArrowRight />}
|
||||
aria-label="breadcrumb"
|
||||
px={theme.spacing(2)}
|
||||
py={theme.spacing(3.5)}
|
||||
width="fit-content"
|
||||
backgroundColor={theme.palette.background.fill}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
lineHeight="18px"
|
||||
sx={{
|
||||
"& .MuiBreadcrumbs-li:not(:last-of-type):hover a": {
|
||||
backgroundColor: theme.palette.other.fill,
|
||||
opacity: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{list.map((item, index) => {
|
||||
return (
|
||||
<Box
|
||||
component="a"
|
||||
key={`${item.name}-${index}`}
|
||||
px={theme.spacing(4)}
|
||||
pt={theme.spacing(2)}
|
||||
pb={theme.spacing(3)}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
onClick={() => navigate(item.path)}
|
||||
sx={{
|
||||
opacity: 0.8,
|
||||
textTransform: "capitalize",
|
||||
"&, &:hover": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</MUIBreadcrumbs>
|
||||
);
|
||||
};
|
||||
|
||||
Breadcrumbs.propTypes = {
|
||||
list: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
}).isRequired
|
||||
).isRequired,
|
||||
list: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
}).isRequired
|
||||
).isRequired,
|
||||
};
|
||||
|
||||
export default Breadcrumbs;
|
||||
|
||||
@@ -6,164 +6,166 @@ import "./index.css";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const BarChart = ({ checks = [] }) => {
|
||||
const theme = useTheme();
|
||||
const [animate, setAnimate] = useState(false);
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
const theme = useTheme();
|
||||
const [animate, setAnimate] = useState(false);
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
useEffect(() => {
|
||||
setAnimate(true);
|
||||
});
|
||||
useEffect(() => {
|
||||
setAnimate(true);
|
||||
});
|
||||
|
||||
// set responseTime to average if there's only one check
|
||||
if (checks.length === 1) {
|
||||
checks[0] = { ...checks[0], responseTime: 50 };
|
||||
}
|
||||
// set responseTime to average if there's only one check
|
||||
if (checks.length === 1) {
|
||||
checks[0] = { ...checks[0], responseTime: 50 };
|
||||
}
|
||||
|
||||
if (checks.length !== 25) {
|
||||
const placeholders = Array(25 - checks.length).fill("placeholder");
|
||||
checks = [...checks, ...placeholders];
|
||||
}
|
||||
if (checks.length !== 25) {
|
||||
const placeholders = Array(25 - checks.length).fill("placeholder");
|
||||
checks = [...checks, ...placeholders];
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
flexWrap="nowrap"
|
||||
gap={theme.spacing(1.5)}
|
||||
height="50px"
|
||||
width="fit-content"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
sx={{
|
||||
cursor: "default",
|
||||
}}
|
||||
>
|
||||
{checks.map((check, index) =>
|
||||
check === "placeholder" ? (
|
||||
<Box
|
||||
key={`${check}-${index}`}
|
||||
position="relative"
|
||||
width={theme.spacing(4.5)}
|
||||
height="100%"
|
||||
backgroundColor={theme.palette.background.fill}
|
||||
sx={{
|
||||
borderRadius: theme.spacing(1.5),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
<Typography>
|
||||
{formatDateWithTz(
|
||||
check.createdAt,
|
||||
"ddd, MMMM D, YYYY, HH:mm A",
|
||||
uiTimezone
|
||||
)}
|
||||
</Typography>
|
||||
<Box mt={theme.spacing(2)}>
|
||||
<Box
|
||||
display="inline-block"
|
||||
width={theme.spacing(4)}
|
||||
height={theme.spacing(4)}
|
||||
backgroundColor={
|
||||
check.status
|
||||
? theme.palette.success.main
|
||||
: theme.palette.error.text
|
||||
}
|
||||
sx={{ borderRadius: "50%" }}
|
||||
/>
|
||||
<Stack
|
||||
display="inline-flex"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
ml={theme.spacing(2)}
|
||||
gap={theme.spacing(12)}
|
||||
>
|
||||
<Typography component="span" sx={{ opacity: 0.8 }}>
|
||||
Response Time
|
||||
</Typography>
|
||||
<Typography component="span">
|
||||
{check.originalResponseTime}
|
||||
<Typography component="span" sx={{ opacity: 0.8 }}>
|
||||
{" "}
|
||||
ms
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
placement="top"
|
||||
key={`check-${check?._id}`}
|
||||
slotProps={{
|
||||
popper: {
|
||||
className: "bar-tooltip",
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -10],
|
||||
},
|
||||
},
|
||||
],
|
||||
sx: {
|
||||
"& .MuiTooltip-tooltip": {
|
||||
backgroundColor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.dark,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxShadow: theme.shape.boxShadow,
|
||||
px: theme.spacing(4),
|
||||
py: theme.spacing(2),
|
||||
},
|
||||
"& .MuiTooltip-tooltip p": {
|
||||
fontSize: 12,
|
||||
color: theme.palette.text.tertiary,
|
||||
fontWeight: 500,
|
||||
},
|
||||
"& .MuiTooltip-tooltip span": {
|
||||
fontSize: 11,
|
||||
color: theme.palette.text.tertiary,
|
||||
fontWeight: 600,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
position="relative"
|
||||
width="9px"
|
||||
height="100%"
|
||||
backgroundColor={
|
||||
check.status ? theme.palette.success.bg : theme.palette.error.bg
|
||||
}
|
||||
sx={{
|
||||
borderRadius: theme.spacing(1.5),
|
||||
"&:hover > .MuiBox-root": {
|
||||
filter: "brightness(0.8)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom={0}
|
||||
width="100%"
|
||||
height={`${animate ? check.responseTime : 0}%`}
|
||||
backgroundColor={
|
||||
check.status
|
||||
? theme.palette.success.main
|
||||
: theme.palette.error.text
|
||||
}
|
||||
sx={{
|
||||
borderRadius: theme.spacing(1.5),
|
||||
transition: "height 600ms cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
flexWrap="nowrap"
|
||||
gap={theme.spacing(1.5)}
|
||||
height="50px"
|
||||
width="fit-content"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
sx={{
|
||||
cursor: "default",
|
||||
}}
|
||||
>
|
||||
{checks.map((check, index) =>
|
||||
check === "placeholder" ? (
|
||||
<Box
|
||||
key={`${check}-${index}`}
|
||||
position="relative"
|
||||
width={theme.spacing(4.5)}
|
||||
height="100%"
|
||||
backgroundColor={theme.palette.background.fill}
|
||||
sx={{
|
||||
borderRadius: theme.spacing(1.5),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
<Typography>
|
||||
{formatDateWithTz(
|
||||
check.createdAt,
|
||||
"ddd, MMMM D, YYYY, HH:mm A",
|
||||
uiTimezone
|
||||
)}
|
||||
</Typography>
|
||||
<Box mt={theme.spacing(2)}>
|
||||
<Box
|
||||
display="inline-block"
|
||||
width={theme.spacing(4)}
|
||||
height={theme.spacing(4)}
|
||||
backgroundColor={
|
||||
check.status ? theme.palette.success.main : theme.palette.error.text
|
||||
}
|
||||
sx={{ borderRadius: "50%" }}
|
||||
/>
|
||||
<Stack
|
||||
display="inline-flex"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
ml={theme.spacing(2)}
|
||||
gap={theme.spacing(12)}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
Response Time
|
||||
</Typography>
|
||||
<Typography component="span">
|
||||
{check.originalResponseTime}
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
{" "}
|
||||
ms
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
placement="top"
|
||||
key={`check-${check?._id}`}
|
||||
slotProps={{
|
||||
popper: {
|
||||
className: "bar-tooltip",
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -10],
|
||||
},
|
||||
},
|
||||
],
|
||||
sx: {
|
||||
"& .MuiTooltip-tooltip": {
|
||||
backgroundColor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.dark,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxShadow: theme.shape.boxShadow,
|
||||
px: theme.spacing(4),
|
||||
py: theme.spacing(2),
|
||||
},
|
||||
"& .MuiTooltip-tooltip p": {
|
||||
fontSize: 12,
|
||||
color: theme.palette.text.tertiary,
|
||||
fontWeight: 500,
|
||||
},
|
||||
"& .MuiTooltip-tooltip span": {
|
||||
fontSize: 11,
|
||||
color: theme.palette.text.tertiary,
|
||||
fontWeight: 600,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
position="relative"
|
||||
width="9px"
|
||||
height="100%"
|
||||
backgroundColor={
|
||||
check.status ? theme.palette.success.bg : theme.palette.error.bg
|
||||
}
|
||||
sx={{
|
||||
borderRadius: theme.spacing(1.5),
|
||||
"&:hover > .MuiBox-root": {
|
||||
filter: "brightness(0.8)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom={0}
|
||||
width="100%"
|
||||
height={`${animate ? check.responseTime : 0}%`}
|
||||
backgroundColor={
|
||||
check.status ? theme.palette.success.main : theme.palette.error.text
|
||||
}
|
||||
sx={{
|
||||
borderRadius: theme.spacing(1.5),
|
||||
transition: "height 600ms cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default BarChart;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
Tooltip,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
Text,
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
Tooltip,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
Text,
|
||||
} from "recharts";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
@@ -16,181 +16,197 @@ import { formatDateWithTz } from "../../../Utils/timeUtils";
|
||||
import "./index.css";
|
||||
|
||||
const CustomToolTip = ({ active, payload, label }) => {
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
const theme = useTheme();
|
||||
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>
|
||||
<Box mt={theme.spacing(1)}>
|
||||
<Box
|
||||
display="inline-block"
|
||||
width={theme.spacing(4)}
|
||||
height={theme.spacing(4)}
|
||||
backgroundColor={theme.palette.primary.main}
|
||||
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 }}>
|
||||
Response Time
|
||||
</Typography>{" "}
|
||||
<Typography component="span">
|
||||
{payload[0].payload.originalResponseTime}
|
||||
<Typography component="span" sx={{ opacity: 0.8 }}>
|
||||
{" "}
|
||||
ms
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
{/* Display original value */}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
const theme = useTheme();
|
||||
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>
|
||||
<Box mt={theme.spacing(1)}>
|
||||
<Box
|
||||
display="inline-block"
|
||||
width={theme.spacing(4)}
|
||||
height={theme.spacing(4)}
|
||||
backgroundColor={theme.palette.primary.main}
|
||||
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 }}
|
||||
>
|
||||
Response Time
|
||||
</Typography>{" "}
|
||||
<Typography component="span">
|
||||
{payload[0].payload.originalResponseTime}
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
{" "}
|
||||
ms
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
{/* Display original value */}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const CustomTick = ({ x, y, payload, index }) => {
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
// Render nothing for the first tick
|
||||
if (index === 0) return null;
|
||||
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>
|
||||
);
|
||||
// Render nothing for the first tick
|
||||
if (index === 0) return null;
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
CustomTick.propTypes = {
|
||||
x: PropTypes.number,
|
||||
y: PropTypes.number,
|
||||
payload: PropTypes.object,
|
||||
index: PropTypes.number,
|
||||
x: PropTypes.number,
|
||||
y: PropTypes.number,
|
||||
payload: PropTypes.object,
|
||||
index: PropTypes.number,
|
||||
};
|
||||
|
||||
const MonitorDetailsAreaChart = ({ checks }) => {
|
||||
const theme = useTheme();
|
||||
const memoizedChecks = useMemo(() => checks, [checks[0]]);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const theme = useTheme();
|
||||
const memoizedChecks = useMemo(() => checks, [checks[0]]);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" minWidth={25} height={220}>
|
||||
<AreaChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={memoizedChecks}
|
||||
margin={{
|
||||
top: 10,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
onMouseMove={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<CartesianGrid
|
||||
stroke={theme.palette.border.light}
|
||||
strokeWidth={1}
|
||||
strokeOpacity={1}
|
||||
fill="transparent"
|
||||
vertical={false}
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={theme.palette.primary.main}
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={theme.palette.primary.light}
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<XAxis
|
||||
stroke={theme.palette.border.dark}
|
||||
dataKey="createdAt"
|
||||
tick={<CustomTick />}
|
||||
minTickGap={0}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
height={20}
|
||||
interval="equidistantPreserveStart"
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={{ stroke: theme.palette.border.light }}
|
||||
content={<CustomToolTip />}
|
||||
wrapperStyle={{ pointerEvents: "none" }}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="responseTime"
|
||||
stroke={theme.palette.primary.main}
|
||||
fill="url(#colorUv)"
|
||||
strokeWidth={isHovered ? 2.5 : 1.5}
|
||||
activeDot={{ stroke: theme.palette.background.main, r: 5 }}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
return (
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
minWidth={25}
|
||||
height={220}
|
||||
>
|
||||
<AreaChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={memoizedChecks}
|
||||
margin={{
|
||||
top: 10,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
onMouseMove={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<CartesianGrid
|
||||
stroke={theme.palette.border.light}
|
||||
strokeWidth={1}
|
||||
strokeOpacity={1}
|
||||
fill="transparent"
|
||||
vertical={false}
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="colorUv"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={theme.palette.primary.main}
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={theme.palette.primary.light}
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<XAxis
|
||||
stroke={theme.palette.border.dark}
|
||||
dataKey="createdAt"
|
||||
tick={<CustomTick />}
|
||||
minTickGap={0}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
height={20}
|
||||
interval="equidistantPreserveStart"
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={{ stroke: theme.palette.border.light }}
|
||||
content={<CustomToolTip />}
|
||||
wrapperStyle={{ pointerEvents: "none" }}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="responseTime"
|
||||
stroke={theme.palette.primary.main}
|
||||
fill="url(#colorUv)"
|
||||
strokeWidth={isHovered ? 2.5 : 1.5}
|
||||
activeDot={{ stroke: theme.palette.background.main, r: 5 }}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
|
||||
MonitorDetailsAreaChart.propTypes = {
|
||||
checks: PropTypes.array,
|
||||
checks: PropTypes.array,
|
||||
};
|
||||
|
||||
CustomToolTip.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
payload: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
payload: PropTypes.shape({
|
||||
originalResponseTime: PropTypes.number.isRequired,
|
||||
}).isRequired,
|
||||
})
|
||||
),
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
active: PropTypes.bool,
|
||||
payload: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
payload: PropTypes.shape({
|
||||
originalResponseTime: PropTypes.number.isRequired,
|
||||
}).isRequired,
|
||||
})
|
||||
),
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
export default MonitorDetailsAreaChart;
|
||||
|
||||
@@ -21,50 +21,49 @@ 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 theme = useTheme();
|
||||
const colors = {
|
||||
success: theme.palette.success.main,
|
||||
error: theme.palette.error.text,
|
||||
info: theme.palette.info.border,
|
||||
};
|
||||
const theme = useTheme();
|
||||
const colors = {
|
||||
success: theme.palette.success.main,
|
||||
error: theme.palette.error.text,
|
||||
info: theme.palette.info.border,
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
className="check"
|
||||
gap={outlined ? theme.spacing(6) : theme.spacing(4)}
|
||||
alignItems="center"
|
||||
>
|
||||
{outlined ? (
|
||||
<CheckOutlined alt="check" />
|
||||
) : (
|
||||
<Box
|
||||
lineHeight={0}
|
||||
sx={{
|
||||
"& svg > path": { fill: colors[variant] },
|
||||
}}
|
||||
>
|
||||
<CheckGrey alt="form checks" />
|
||||
</Box>
|
||||
)}
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
color:
|
||||
variant === "info" ? theme.palette.text.tertiary : colors[variant],
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
className="check"
|
||||
gap={outlined ? theme.spacing(6) : theme.spacing(4)}
|
||||
alignItems="center"
|
||||
>
|
||||
{outlined ? (
|
||||
<CheckOutlined alt="check" />
|
||||
) : (
|
||||
<Box
|
||||
lineHeight={0}
|
||||
sx={{
|
||||
"& svg > path": { fill: colors[variant] },
|
||||
}}
|
||||
>
|
||||
<CheckGrey alt="form checks" />
|
||||
</Box>
|
||||
)}
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
color: variant === "info" ? theme.palette.text.tertiary : colors[variant],
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Check.propTypes = {
|
||||
text: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
|
||||
variant: PropTypes.oneOf(["info", "error", "success"]),
|
||||
outlined: PropTypes.bool,
|
||||
text: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
|
||||
variant: PropTypes.oneOf(["info", "error", "success"]),
|
||||
outlined: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Check;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -1,80 +1,103 @@
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { Button, Modal, Stack, Typography } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
const Dialog = ({ modelTitle, modelDescription,
|
||||
open, onClose, title, confirmationBtnLbl, confirmationBtnOnClick, cancelBtnLbl, cancelBtnOnClick, theme, isLoading, description }) => {
|
||||
return <Modal
|
||||
aria-labelledby={modelTitle}
|
||||
aria-describedby={modelDescription}
|
||||
open={open}
|
||||
onClose={onClose} >
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 400,
|
||||
bgcolor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxShadow: 24,
|
||||
p: theme.spacing(15),
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography id={modelTitle} component="h2"
|
||||
fontSize={16}
|
||||
color={theme.palette.text.primary}
|
||||
fontWeight={600}>
|
||||
{title}
|
||||
</Typography>
|
||||
{description && <Typography id={modelDescription} color={theme.palette.text.tertiary}>
|
||||
{description}
|
||||
</Typography>}
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
mt={theme.spacing(12)}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={cancelBtnOnClick}
|
||||
>
|
||||
{cancelBtnLbl}
|
||||
</Button>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="error"
|
||||
loading= {isLoading}
|
||||
onClick={confirmationBtnOnClick}
|
||||
>
|
||||
{confirmationBtnLbl}
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
}
|
||||
const Dialog = ({
|
||||
modelTitle,
|
||||
modelDescription,
|
||||
open,
|
||||
onClose,
|
||||
title,
|
||||
confirmationBtnLbl,
|
||||
confirmationBtnOnClick,
|
||||
cancelBtnLbl,
|
||||
cancelBtnOnClick,
|
||||
theme,
|
||||
isLoading,
|
||||
description,
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
aria-labelledby={modelTitle}
|
||||
aria-describedby={modelDescription}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 400,
|
||||
bgcolor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxShadow: 24,
|
||||
p: theme.spacing(15),
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
id={modelTitle}
|
||||
component="h2"
|
||||
fontSize={16}
|
||||
color={theme.palette.text.primary}
|
||||
fontWeight={600}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
{description && (
|
||||
<Typography
|
||||
id={modelDescription}
|
||||
color={theme.palette.text.tertiary}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
)}
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
mt={theme.spacing(12)}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={cancelBtnOnClick}
|
||||
>
|
||||
{cancelBtnLbl}
|
||||
</Button>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="error"
|
||||
loading={isLoading}
|
||||
onClick={confirmationBtnOnClick}
|
||||
>
|
||||
{confirmationBtnLbl}
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
Dialog.propTypes = {
|
||||
modelTitle: PropTypes.string.isRequired,
|
||||
modelDescription: PropTypes.string.isRequired,
|
||||
open: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
confirmationBtnLbl: PropTypes.string.isRequired,
|
||||
confirmationBtnOnClick: PropTypes.func.isRequired,
|
||||
cancelBtnLbl: PropTypes.string.isRequired,
|
||||
cancelBtnOnClick: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
description: PropTypes.string
|
||||
}
|
||||
modelTitle: PropTypes.string.isRequired,
|
||||
modelDescription: PropTypes.string.isRequired,
|
||||
open: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
confirmationBtnLbl: PropTypes.string.isRequired,
|
||||
confirmationBtnOnClick: PropTypes.func.isRequired,
|
||||
cancelBtnLbl: PropTypes.string.isRequired,
|
||||
cancelBtnOnClick: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
description: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Dialog
|
||||
export default Dialog;
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
[class*="fallback__"] {
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
margin-top: 100px;
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
margin-top: 100px;
|
||||
}
|
||||
[class*="fallback__"] h1.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
font-weight: 600;
|
||||
font-size: var(--env-var-font-size-large);
|
||||
font-weight: 600;
|
||||
}
|
||||
[class*="fallback__"] button.MuiButtonBase-root,
|
||||
[class*="fallback__"] .check {
|
||||
width: max-content;
|
||||
width: max-content;
|
||||
}
|
||||
[class*="fallback__"] button.MuiButtonBase-root {
|
||||
height: 34px;
|
||||
height: 34px;
|
||||
}
|
||||
[class*="fallback__"] .check span.MuiTypography-root,
|
||||
[class*="fallback__"] button.MuiButtonBase-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
|
||||
.fallback__status > .MuiStack-root {
|
||||
margin-left: var(--env-var-spacing-2);
|
||||
margin-left: var(--env-var-spacing-2);
|
||||
}
|
||||
|
||||
@@ -21,67 +21,71 @@ import "./index.css";
|
||||
*/
|
||||
|
||||
const Fallback = ({ title, checks, link = "/", isAdmin }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className={`fallback__${title.trim().split(" ")[0]}`}
|
||||
alignItems="center"
|
||||
gap={theme.spacing(20)}
|
||||
>
|
||||
{mode === "light" ? (
|
||||
<Skeleton style={{ zIndex: 1 }} />
|
||||
) : (
|
||||
<SkeletonDark style={{ zIndex: 1 }} />
|
||||
)}
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
sx={{
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.border.light,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(4)} maxWidth={"275px"} zIndex={1}>
|
||||
<Typography
|
||||
component="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.text.tertiary}
|
||||
>
|
||||
A {title} is used to:
|
||||
</Typography>
|
||||
{checks.map((check, index) => (
|
||||
<Check
|
||||
text={check}
|
||||
key={`${title.trim().split(" ")[0]}-${index}`}
|
||||
outlined={true}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
{/* TODO - display a different fallback if user is not an admin*/}
|
||||
{isAdmin && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ alignSelf: "center" }}
|
||||
onClick={() => navigate(link)}
|
||||
>
|
||||
Let's create your {title}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
className={`fallback__${title.trim().split(" ")[0]}`}
|
||||
alignItems="center"
|
||||
gap={theme.spacing(20)}
|
||||
>
|
||||
{mode === "light" ? (
|
||||
<Skeleton style={{ zIndex: 1 }} />
|
||||
) : (
|
||||
<SkeletonDark style={{ zIndex: 1 }} />
|
||||
)}
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
sx={{
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.border.light,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
maxWidth={"275px"}
|
||||
zIndex={1}
|
||||
>
|
||||
<Typography
|
||||
component="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.text.tertiary}
|
||||
>
|
||||
A {title} is used to:
|
||||
</Typography>
|
||||
{checks.map((check, index) => (
|
||||
<Check
|
||||
text={check}
|
||||
key={`${title.trim().split(" ")[0]}-${index}`}
|
||||
outlined={true}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
{/* TODO - display a different fallback if user is not an admin*/}
|
||||
{isAdmin && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ alignSelf: "center" }}
|
||||
onClick={() => navigate(link)}
|
||||
>
|
||||
Let's create your {title}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Fallback.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
checks: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
link: PropTypes.string,
|
||||
isAdmin: PropTypes.bool,
|
||||
title: PropTypes.string.isRequired,
|
||||
checks: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
link: PropTypes.string,
|
||||
isAdmin: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Fallback;
|
||||
|
||||
@@ -29,68 +29,68 @@ import "./index.css";
|
||||
*/
|
||||
|
||||
const Checkbox = ({
|
||||
id,
|
||||
label,
|
||||
size = "medium",
|
||||
isChecked,
|
||||
value,
|
||||
onChange,
|
||||
isDisabled,
|
||||
id,
|
||||
label,
|
||||
size = "medium",
|
||||
isChecked,
|
||||
value,
|
||||
onChange,
|
||||
isDisabled,
|
||||
}) => {
|
||||
const sizes = { small: "14px", medium: "16px", large: "18px" };
|
||||
const theme = useTheme();
|
||||
const sizes = { small: "14px", medium: "16px", large: "18px" };
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<FormControlLabel
|
||||
className="checkbox-wrapper"
|
||||
control={
|
||||
<MuiCheckbox
|
||||
checked={isDisabled ? false : isChecked}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
icon={<CheckboxOutline />}
|
||||
checkedIcon={<CheckboxFilled />}
|
||||
inputProps={{
|
||||
"aria-label": "controlled checkbox",
|
||||
id: id,
|
||||
}}
|
||||
sx={{
|
||||
"&:hover": { backgroundColor: "transparent" },
|
||||
"& svg": { width: sizes[size], height: sizes[size] },
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={label}
|
||||
disabled={isDisabled}
|
||||
sx={{
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
p: theme.spacing(2.5),
|
||||
m: theme.spacing(-2.5),
|
||||
"& .MuiButtonBase-root": {
|
||||
width: theme.spacing(10),
|
||||
p: 0,
|
||||
mr: theme.spacing(6),
|
||||
},
|
||||
"&:not(:has(.Mui-disabled)):hover": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
"& span.MuiTypography-root": {
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<FormControlLabel
|
||||
className="checkbox-wrapper"
|
||||
control={
|
||||
<MuiCheckbox
|
||||
checked={isDisabled ? false : isChecked}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
icon={<CheckboxOutline />}
|
||||
checkedIcon={<CheckboxFilled />}
|
||||
inputProps={{
|
||||
"aria-label": "controlled checkbox",
|
||||
id: id,
|
||||
}}
|
||||
sx={{
|
||||
"&:hover": { backgroundColor: "transparent" },
|
||||
"& svg": { width: sizes[size], height: sizes[size] },
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={label}
|
||||
disabled={isDisabled}
|
||||
sx={{
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
p: theme.spacing(2.5),
|
||||
m: theme.spacing(-2.5),
|
||||
"& .MuiButtonBase-root": {
|
||||
width: theme.spacing(10),
|
||||
p: 0,
|
||||
mr: theme.spacing(6),
|
||||
},
|
||||
"&:not(:has(.Mui-disabled)):hover": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
"& span.MuiTypography-root": {
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Checkbox.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
size: PropTypes.oneOf(["small", "medium", "large"]),
|
||||
isChecked: PropTypes.bool.isRequired,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
isDisabled: PropTypes.bool,
|
||||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
size: PropTypes.oneOf(["small", "medium", "large"]),
|
||||
isChecked: PropTypes.bool.isRequired,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
isDisabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Checkbox;
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
.field {
|
||||
min-width: 250px;
|
||||
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);
|
||||
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);
|
||||
position: relative;
|
||||
opacity: 0.8;
|
||||
padding-right: var(--env-var-spacing-1-minus);
|
||||
}
|
||||
.field .MuiInputBase-root:has(input) {
|
||||
height: 34px;
|
||||
height: 34px;
|
||||
}
|
||||
.field .MuiInputBase-root:has(.MuiInputAdornment-root) {
|
||||
padding-right: var(--env-var-spacing-1-minus);
|
||||
padding-right: var(--env-var-spacing-1-minus);
|
||||
}
|
||||
|
||||
.field input {
|
||||
height: 100%;
|
||||
padding: 0 var(--env-var-spacing-1-minus);
|
||||
height: 100%;
|
||||
padding: 0 var(--env-var-spacing-1-minus);
|
||||
}
|
||||
.field .MuiInputBase-root:has(textarea) {
|
||||
padding: var(--env-var-spacing-1-minus);
|
||||
padding: var(--env-var-spacing-1-minus);
|
||||
}
|
||||
|
||||
.register-page .field .MuiOutlinedInput-root fieldset,
|
||||
@@ -36,5 +36,5 @@
|
||||
.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);
|
||||
border-radius: var(--env-var-radius-2);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
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 { 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";
|
||||
@@ -33,208 +27,199 @@ import "./index.css";
|
||||
*/
|
||||
|
||||
const Field = forwardRef(
|
||||
(
|
||||
{
|
||||
type = "text",
|
||||
id,
|
||||
name,
|
||||
label,
|
||||
https,
|
||||
isRequired,
|
||||
isOptional,
|
||||
optionalLabel,
|
||||
autoComplete,
|
||||
placeholder,
|
||||
value,
|
||||
onChange,
|
||||
onInput,
|
||||
error,
|
||||
disabled,
|
||||
hidden,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
(
|
||||
{
|
||||
type = "text",
|
||||
id,
|
||||
name,
|
||||
label,
|
||||
https,
|
||||
isRequired,
|
||||
isOptional,
|
||||
optionalLabel,
|
||||
autoComplete,
|
||||
placeholder,
|
||||
value,
|
||||
onChange,
|
||||
onInput,
|
||||
error,
|
||||
disabled,
|
||||
hidden,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [isVisible, setVisible] = useState(false);
|
||||
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}
|
||||
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>
|
||||
);
|
||||
}
|
||||
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}
|
||||
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,
|
||||
onInput: PropTypes.func,
|
||||
error: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
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,
|
||||
onInput: PropTypes.func,
|
||||
error: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Field;
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
.MuiStack-root:has(#modal-update-picture) h1.MuiTypography-root {
|
||||
font-weight: 600;
|
||||
font-weight: 600;
|
||||
}
|
||||
.image-field-wrapper h2.MuiTypography-root,
|
||||
.MuiStack-root:has(#modal-update-picture) button,
|
||||
.MuiStack-root:has(#modal-update-picture) h1.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.image-field-wrapper h2.MuiTypography-root {
|
||||
margin-top: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.image-field-wrapper + p.MuiTypography-root {
|
||||
margin-top: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.image-field-wrapper + p.MuiTypography-root,
|
||||
.image-field-wrapper p.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
}
|
||||
|
||||
@@ -15,133 +15,139 @@ import { checkImage } from "../../../Utils/fileUtils";
|
||||
*/
|
||||
|
||||
const ImageField = ({ id, src, loading, onChange }) => {
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const handleDragEnter = () => {
|
||||
setIsDragging(true);
|
||||
};
|
||||
const handleDragLeave = () => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const handleDragEnter = () => {
|
||||
setIsDragging(true);
|
||||
};
|
||||
const handleDragLeave = () => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!checkImage(src) || loading ? (
|
||||
<>
|
||||
<Box
|
||||
className="image-field-wrapper"
|
||||
mt={theme.spacing(8)}
|
||||
sx={{
|
||||
position: "relative",
|
||||
height: "fit-content",
|
||||
border: "dashed",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
borderColor: isDragging
|
||||
? theme.palette.primary.main
|
||||
: theme.palette.border.light,
|
||||
borderWidth: "2px",
|
||||
transition: "0.2s",
|
||||
"&:hover": {
|
||||
borderColor: theme.palette.primary.main,
|
||||
backgroundColor: "hsl(215, 87%, 51%, 0.05)",
|
||||
},
|
||||
}}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDragLeave}
|
||||
>
|
||||
<TextField
|
||||
id={id}
|
||||
type="file"
|
||||
onChange={onChange}
|
||||
sx={{
|
||||
width: "100%",
|
||||
"& .MuiInputBase-input[type='file']": {
|
||||
opacity: 0,
|
||||
cursor: "pointer",
|
||||
maxWidth: "500px",
|
||||
minHeight: "175px",
|
||||
},
|
||||
"& fieldset": {
|
||||
padding: 0,
|
||||
border: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Stack
|
||||
className="custom-file-text"
|
||||
alignItems="center"
|
||||
gap="4px"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
zIndex: "-1",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
pointerEvents: "none",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
border: `solid ${theme.shape.borderThick}px ${theme.palette.border.light}`,
|
||||
boxShadow: theme.shape.boxShadow,
|
||||
}}
|
||||
>
|
||||
<CloudUploadIcon />
|
||||
</IconButton>
|
||||
<Typography component="h2" color={theme.palette.text.tertiary}>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
color={theme.palette.primary.main}
|
||||
fontWeight={500}
|
||||
>
|
||||
Click to upload
|
||||
</Typography>{" "}
|
||||
or drag and drop
|
||||
</Typography>
|
||||
<Typography
|
||||
component="p"
|
||||
color={theme.palette.text.tertiary}
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
(maximum size: 3MB)
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Typography
|
||||
component="p"
|
||||
color={theme.palette.text.tertiary}
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
Supported formats: JPG, PNG
|
||||
</Typography>
|
||||
</>
|
||||
) : (
|
||||
<Stack direction="row" justifyContent="center">
|
||||
<Box
|
||||
sx={{
|
||||
width: "250px",
|
||||
height: "250px",
|
||||
borderRadius: "50%",
|
||||
overflow: "hidden",
|
||||
backgroundImage: `url(${src})`,
|
||||
backgroundSize: "cover",
|
||||
}}
|
||||
></Box>
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{!checkImage(src) || loading ? (
|
||||
<>
|
||||
<Box
|
||||
className="image-field-wrapper"
|
||||
mt={theme.spacing(8)}
|
||||
sx={{
|
||||
position: "relative",
|
||||
height: "fit-content",
|
||||
border: "dashed",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
borderColor: isDragging
|
||||
? theme.palette.primary.main
|
||||
: theme.palette.border.light,
|
||||
borderWidth: "2px",
|
||||
transition: "0.2s",
|
||||
"&:hover": {
|
||||
borderColor: theme.palette.primary.main,
|
||||
backgroundColor: "hsl(215, 87%, 51%, 0.05)",
|
||||
},
|
||||
}}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDragLeave}
|
||||
>
|
||||
<TextField
|
||||
id={id}
|
||||
type="file"
|
||||
onChange={onChange}
|
||||
sx={{
|
||||
width: "100%",
|
||||
"& .MuiInputBase-input[type='file']": {
|
||||
opacity: 0,
|
||||
cursor: "pointer",
|
||||
maxWidth: "500px",
|
||||
minHeight: "175px",
|
||||
},
|
||||
"& fieldset": {
|
||||
padding: 0,
|
||||
border: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Stack
|
||||
className="custom-file-text"
|
||||
alignItems="center"
|
||||
gap="4px"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
zIndex: "-1",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
pointerEvents: "none",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
border: `solid ${theme.shape.borderThick}px ${theme.palette.border.light}`,
|
||||
boxShadow: theme.shape.boxShadow,
|
||||
}}
|
||||
>
|
||||
<CloudUploadIcon />
|
||||
</IconButton>
|
||||
<Typography
|
||||
component="h2"
|
||||
color={theme.palette.text.tertiary}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
color={theme.palette.primary.main}
|
||||
fontWeight={500}
|
||||
>
|
||||
Click to upload
|
||||
</Typography>{" "}
|
||||
or drag and drop
|
||||
</Typography>
|
||||
<Typography
|
||||
component="p"
|
||||
color={theme.palette.text.tertiary}
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
(maximum size: 3MB)
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Typography
|
||||
component="p"
|
||||
color={theme.palette.text.tertiary}
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
Supported formats: JPG, PNG
|
||||
</Typography>
|
||||
</>
|
||||
) : (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "250px",
|
||||
height: "250px",
|
||||
borderRadius: "50%",
|
||||
overflow: "hidden",
|
||||
backgroundImage: `url(${src})`,
|
||||
backgroundSize: "cover",
|
||||
}}
|
||||
></Box>
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ImageField.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
src: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
src: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ImageField;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -25,62 +25,62 @@ import "./index.css";
|
||||
*/
|
||||
|
||||
const Radio = (props) => {
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<FormControlLabel
|
||||
className="custom-radio-button"
|
||||
checked={props.checked}
|
||||
value={props.value}
|
||||
control={
|
||||
<MUIRadio
|
||||
id={props.id}
|
||||
size={props.size}
|
||||
checkedIcon={<RadioChecked />}
|
||||
sx={{
|
||||
color: "transparent",
|
||||
width: 16,
|
||||
height: 16,
|
||||
boxShadow: "inset 0 0 0 1px #656a74",
|
||||
mt: theme.spacing(0.5),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onChange={props.onChange}
|
||||
label={
|
||||
<>
|
||||
<Typography component="p">{props.title}</Typography>
|
||||
<Typography
|
||||
component="h6"
|
||||
mt={theme.spacing(1)}
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
{props.desc}
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
labelPlacement="end"
|
||||
sx={{
|
||||
alignItems: "flex-start",
|
||||
p: theme.spacing(2.5),
|
||||
m: theme.spacing(-2.5),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
"& .MuiButtonBase-root": {
|
||||
p: 0,
|
||||
mr: theme.spacing(6),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<FormControlLabel
|
||||
className="custom-radio-button"
|
||||
checked={props.checked}
|
||||
value={props.value}
|
||||
control={
|
||||
<MUIRadio
|
||||
id={props.id}
|
||||
size={props.size}
|
||||
checkedIcon={<RadioChecked />}
|
||||
sx={{
|
||||
color: "transparent",
|
||||
width: 16,
|
||||
height: 16,
|
||||
boxShadow: "inset 0 0 0 1px #656a74",
|
||||
mt: theme.spacing(0.5),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onChange={props.onChange}
|
||||
label={
|
||||
<>
|
||||
<Typography component="p">{props.title}</Typography>
|
||||
<Typography
|
||||
component="h6"
|
||||
mt={theme.spacing(1)}
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
{props.desc}
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
labelPlacement="end"
|
||||
sx={{
|
||||
alignItems: "flex-start",
|
||||
p: theme.spacing(2.5),
|
||||
m: theme.spacing(-2.5),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
"& .MuiButtonBase-root": {
|
||||
p: 0,
|
||||
mr: theme.spacing(6),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Radio.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
desc: PropTypes.string,
|
||||
size: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
desc: PropTypes.string,
|
||||
size: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Radio;
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
Box,
|
||||
ListItem,
|
||||
Autocomplete,
|
||||
TextField,
|
||||
Stack,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { Box, ListItem, Autocomplete, TextField, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import SearchIcon from "../../../assets/icons/search.svg?react";
|
||||
|
||||
@@ -24,172 +17,171 @@ import SearchIcon from "../../../assets/icons/search.svg?react";
|
||||
*/
|
||||
|
||||
const SearchAdornment = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box
|
||||
mr={theme.spacing(4)}
|
||||
height={16}
|
||||
sx={{
|
||||
"& svg": {
|
||||
width: 16,
|
||||
height: 16,
|
||||
"& path": {
|
||||
stroke: theme.palette.text.tertiary,
|
||||
strokeWidth: 1.2,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SearchIcon />
|
||||
</Box>
|
||||
);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box
|
||||
mr={theme.spacing(4)}
|
||||
height={16}
|
||||
sx={{
|
||||
"& svg": {
|
||||
width: 16,
|
||||
height: 16,
|
||||
"& path": {
|
||||
stroke: theme.palette.text.tertiary,
|
||||
strokeWidth: 1.2,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SearchIcon />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const Search = ({
|
||||
id,
|
||||
options,
|
||||
filteredBy,
|
||||
secondaryLabel,
|
||||
value,
|
||||
inputValue,
|
||||
handleInputChange,
|
||||
handleChange,
|
||||
sx,
|
||||
multiple = false,
|
||||
isAdorned = true,
|
||||
error,
|
||||
disabled,
|
||||
id,
|
||||
options,
|
||||
filteredBy,
|
||||
secondaryLabel,
|
||||
value,
|
||||
inputValue,
|
||||
handleInputChange,
|
||||
handleChange,
|
||||
sx,
|
||||
multiple = false,
|
||||
isAdorned = true,
|
||||
error,
|
||||
disabled,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
multiple={multiple}
|
||||
id={id}
|
||||
value={value}
|
||||
inputValue={inputValue}
|
||||
onInputChange={(_, newValue) => {
|
||||
handleInputChange(newValue);
|
||||
}}
|
||||
onChange={(_, newValue) => {
|
||||
handleChange && handleChange(newValue);
|
||||
}}
|
||||
fullWidth
|
||||
freeSolo
|
||||
disabled={disabled}
|
||||
disableClearable
|
||||
options={options}
|
||||
getOptionLabel={(option) => option[filteredBy]}
|
||||
renderInput={(params) => (
|
||||
<Stack>
|
||||
<TextField
|
||||
{...params}
|
||||
error={Boolean(error)}
|
||||
placeholder="Type to search"
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
...(isAdorned && { startAdornment: <SearchAdornment /> }),
|
||||
}}
|
||||
sx={{
|
||||
"& fieldset": {
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
"& .MuiOutlinedInput-root:hover:not(:has(input:focus)):not(:has(textarea:focus)) fieldset":
|
||||
{
|
||||
borderColor: theme.palette.border.light,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{error && (
|
||||
<Typography
|
||||
component="span"
|
||||
className="input-error"
|
||||
color={theme.palette.error.text}
|
||||
mt={theme.spacing(2)}
|
||||
sx={{
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
filterOptions={(options, { inputValue }) => {
|
||||
const filtered = options.filter((option) =>
|
||||
option[filteredBy].toLowerCase().includes(inputValue.toLowerCase())
|
||||
);
|
||||
return (
|
||||
<Autocomplete
|
||||
multiple={multiple}
|
||||
id={id}
|
||||
value={value}
|
||||
inputValue={inputValue}
|
||||
onInputChange={(_, newValue) => {
|
||||
handleInputChange(newValue);
|
||||
}}
|
||||
onChange={(_, newValue) => {
|
||||
handleChange && handleChange(newValue);
|
||||
}}
|
||||
fullWidth
|
||||
freeSolo
|
||||
disabled={disabled}
|
||||
disableClearable
|
||||
options={options}
|
||||
getOptionLabel={(option) => option[filteredBy]}
|
||||
renderInput={(params) => (
|
||||
<Stack>
|
||||
<TextField
|
||||
{...params}
|
||||
error={Boolean(error)}
|
||||
placeholder="Type to search"
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
...(isAdorned && { startAdornment: <SearchAdornment /> }),
|
||||
}}
|
||||
sx={{
|
||||
"& fieldset": {
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
"& .MuiOutlinedInput-root:hover:not(:has(input:focus)):not(:has(textarea:focus)) fieldset":
|
||||
{
|
||||
borderColor: theme.palette.border.light,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{error && (
|
||||
<Typography
|
||||
component="span"
|
||||
className="input-error"
|
||||
color={theme.palette.error.text}
|
||||
mt={theme.spacing(2)}
|
||||
sx={{
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
filterOptions={(options, { inputValue }) => {
|
||||
const filtered = options.filter((option) =>
|
||||
option[filteredBy].toLowerCase().includes(inputValue.toLowerCase())
|
||||
);
|
||||
|
||||
if (filtered.length === 0) {
|
||||
return [{ [filteredBy]: "No monitors found", noOptions: true }];
|
||||
}
|
||||
return filtered;
|
||||
}}
|
||||
renderOption={(props, option) => {
|
||||
const { key, ...optionProps } = props;
|
||||
return (
|
||||
<ListItem
|
||||
key={key}
|
||||
{...optionProps}
|
||||
sx={
|
||||
option.noOptions
|
||||
? {
|
||||
pointerEvents: "none",
|
||||
backgroundColor: theme.palette.background.main,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{option[filteredBy] +
|
||||
(secondaryLabel ? ` (${option[secondaryLabel]})` : "")}
|
||||
</ListItem>
|
||||
);
|
||||
}}
|
||||
slotProps={{
|
||||
popper: {
|
||||
keepMounted: true,
|
||||
sx: {
|
||||
"& ul": { p: 2 },
|
||||
"& li.MuiAutocomplete-option": {
|
||||
color: theme.palette.text.secondary,
|
||||
px: 4,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
"& .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true'], & .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true'].Mui-focused, & .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true']:hover":
|
||||
{
|
||||
backgroundColor: theme.palette.background.fill,
|
||||
},
|
||||
"& .MuiAutocomplete-noOptions": {
|
||||
px: theme.spacing(6),
|
||||
py: theme.spacing(5),
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
height: 34,
|
||||
"&.MuiAutocomplete-root .MuiAutocomplete-input": { p: 0 },
|
||||
...sx,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
if (filtered.length === 0) {
|
||||
return [{ [filteredBy]: "No monitors found", noOptions: true }];
|
||||
}
|
||||
return filtered;
|
||||
}}
|
||||
renderOption={(props, option) => {
|
||||
const { key, ...optionProps } = props;
|
||||
return (
|
||||
<ListItem
|
||||
key={key}
|
||||
{...optionProps}
|
||||
sx={
|
||||
option.noOptions
|
||||
? {
|
||||
pointerEvents: "none",
|
||||
backgroundColor: theme.palette.background.main,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{option[filteredBy] + (secondaryLabel ? ` (${option[secondaryLabel]})` : "")}
|
||||
</ListItem>
|
||||
);
|
||||
}}
|
||||
slotProps={{
|
||||
popper: {
|
||||
keepMounted: true,
|
||||
sx: {
|
||||
"& ul": { p: 2 },
|
||||
"& li.MuiAutocomplete-option": {
|
||||
color: theme.palette.text.secondary,
|
||||
px: 4,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
"& .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true'], & .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true'].Mui-focused, & .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true']:hover":
|
||||
{
|
||||
backgroundColor: theme.palette.background.fill,
|
||||
},
|
||||
"& .MuiAutocomplete-noOptions": {
|
||||
px: theme.spacing(6),
|
||||
py: theme.spacing(5),
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
height: 34,
|
||||
"&.MuiAutocomplete-root .MuiAutocomplete-input": { p: 0 },
|
||||
...sx,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Search.propTypes = {
|
||||
id: PropTypes.string,
|
||||
multiple: PropTypes.bool,
|
||||
options: PropTypes.array.isRequired,
|
||||
filteredBy: PropTypes.string.isRequired,
|
||||
secondaryLabel: PropTypes.string,
|
||||
value: PropTypes.array,
|
||||
inputValue: PropTypes.string.isRequired,
|
||||
handleInputChange: PropTypes.func.isRequired,
|
||||
handleChange: PropTypes.func,
|
||||
isAdorned: PropTypes.bool,
|
||||
sx: PropTypes.object,
|
||||
error: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
id: PropTypes.string,
|
||||
multiple: PropTypes.bool,
|
||||
options: PropTypes.array.isRequired,
|
||||
filteredBy: PropTypes.string.isRequired,
|
||||
secondaryLabel: PropTypes.string,
|
||||
value: PropTypes.array,
|
||||
inputValue: PropTypes.string.isRequired,
|
||||
handleInputChange: PropTypes.func.isRequired,
|
||||
handleChange: PropTypes.func,
|
||||
isAdorned: PropTypes.bool,
|
||||
sx: PropTypes.object,
|
||||
error: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Search;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.select-wrapper .select-component > .MuiSelect-select {
|
||||
padding: 0 10px;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
padding: 0 10px;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import {
|
||||
MenuItem,
|
||||
Select as MuiSelect,
|
||||
Stack,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { MenuItem, Select as MuiSelect, Stack, Typography } from "@mui/material";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
|
||||
import "./index.css";
|
||||
@@ -44,105 +39,108 @@ import "./index.css";
|
||||
*/
|
||||
|
||||
const Select = ({
|
||||
id,
|
||||
label,
|
||||
placeholder,
|
||||
isHidden,
|
||||
value,
|
||||
items,
|
||||
onChange,
|
||||
sx,
|
||||
name = "",
|
||||
id,
|
||||
label,
|
||||
placeholder,
|
||||
isHidden,
|
||||
value,
|
||||
items,
|
||||
onChange,
|
||||
sx,
|
||||
name = "",
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const itemStyles = {
|
||||
fontSize: "var(--env-var-font-size-medium)",
|
||||
color: theme.palette.text.tertiary,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
margin: theme.spacing(2),
|
||||
};
|
||||
const theme = useTheme();
|
||||
const itemStyles = {
|
||||
fontSize: "var(--env-var-font-size-medium)",
|
||||
color: theme.palette.text.tertiary,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
margin: theme.spacing(2),
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(2)} className="select-wrapper">
|
||||
{label && (
|
||||
<Typography
|
||||
component="h3"
|
||||
color={theme.palette.text.secondary}
|
||||
fontWeight={500}
|
||||
fontSize={13}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
)}
|
||||
<MuiSelect
|
||||
className="select-component"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
displayEmpty
|
||||
name={name}
|
||||
inputProps={{ id: id }}
|
||||
IconComponent={KeyboardArrowDownIcon}
|
||||
MenuProps={{ disableScrollLock: true }}
|
||||
sx={{
|
||||
fontSize: 13,
|
||||
minWidth: "125px",
|
||||
"& fieldset": {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
borderColor: theme.palette.border.dark,
|
||||
},
|
||||
"&:not(.Mui-focused):hover fieldset": {
|
||||
borderColor: theme.palette.border.dark,
|
||||
},
|
||||
"& svg path": {
|
||||
fill: theme.palette.other.icon,
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{placeholder && (
|
||||
<MenuItem
|
||||
className="select-placeholder"
|
||||
value="0"
|
||||
sx={{
|
||||
display: isHidden ? "none" : "flex",
|
||||
visibility: isHidden ? "none" : "visible",
|
||||
...itemStyles,
|
||||
}}
|
||||
>
|
||||
{placeholder}
|
||||
</MenuItem>
|
||||
)}
|
||||
{items.map((item) => (
|
||||
<MenuItem
|
||||
value={item._id}
|
||||
key={`${id}-${item._id}`}
|
||||
sx={{
|
||||
...itemStyles,
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MuiSelect>
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
className="select-wrapper"
|
||||
>
|
||||
{label && (
|
||||
<Typography
|
||||
component="h3"
|
||||
color={theme.palette.text.secondary}
|
||||
fontWeight={500}
|
||||
fontSize={13}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
)}
|
||||
<MuiSelect
|
||||
className="select-component"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
displayEmpty
|
||||
name={name}
|
||||
inputProps={{ id: id }}
|
||||
IconComponent={KeyboardArrowDownIcon}
|
||||
MenuProps={{ disableScrollLock: true }}
|
||||
sx={{
|
||||
fontSize: 13,
|
||||
minWidth: "125px",
|
||||
"& fieldset": {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
borderColor: theme.palette.border.dark,
|
||||
},
|
||||
"&:not(.Mui-focused):hover fieldset": {
|
||||
borderColor: theme.palette.border.dark,
|
||||
},
|
||||
"& svg path": {
|
||||
fill: theme.palette.other.icon,
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{placeholder && (
|
||||
<MenuItem
|
||||
className="select-placeholder"
|
||||
value="0"
|
||||
sx={{
|
||||
display: isHidden ? "none" : "flex",
|
||||
visibility: isHidden ? "none" : "visible",
|
||||
...itemStyles,
|
||||
}}
|
||||
>
|
||||
{placeholder}
|
||||
</MenuItem>
|
||||
)}
|
||||
{items.map((item) => (
|
||||
<MenuItem
|
||||
value={item._id}
|
||||
key={`${id}-${item._id}`}
|
||||
sx={{
|
||||
...itemStyles,
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MuiSelect>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Select.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
isHidden: PropTypes.bool,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
items: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
sx: PropTypes.object,
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
isHidden: PropTypes.bool,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
items: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
sx: PropTypes.object,
|
||||
};
|
||||
|
||||
export default Select;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.label {
|
||||
border: 1px solid #000;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
line-height: normal;
|
||||
border: 1px solid #000;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
@@ -20,56 +20,56 @@ import "./index.css";
|
||||
*/
|
||||
|
||||
const BaseLabel = ({ label, styles, children }) => {
|
||||
const theme = useTheme();
|
||||
// Grab the default borderRadius from the theme to match button style
|
||||
const { borderRadius } = theme.shape;
|
||||
// Calculate padding for the label to mimic button. Appears to scale correctly, not 100% sure though.
|
||||
const padding = theme.spacing(1 * 0.75, 2);
|
||||
const theme = useTheme();
|
||||
// Grab the default borderRadius from the theme to match button style
|
||||
const { borderRadius } = theme.shape;
|
||||
// Calculate padding for the label to mimic button. Appears to scale correctly, not 100% sure though.
|
||||
const padding = theme.spacing(1 * 0.75, 2);
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="label"
|
||||
sx={{
|
||||
borderRadius: borderRadius,
|
||||
borderColor: theme.palette.text.tertiary,
|
||||
color: theme.palette.text.tertiary,
|
||||
padding: padding,
|
||||
...styles,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
{label}
|
||||
</Box>
|
||||
);
|
||||
return (
|
||||
<Box
|
||||
className="label"
|
||||
sx={{
|
||||
borderRadius: borderRadius,
|
||||
borderColor: theme.palette.text.tertiary,
|
||||
color: theme.palette.text.tertiary,
|
||||
padding: padding,
|
||||
...styles,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
{label}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
BaseLabel.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
styles: PropTypes.shape({
|
||||
color: PropTypes.string,
|
||||
backgroundColor: PropTypes.string,
|
||||
}),
|
||||
children: PropTypes.node,
|
||||
label: PropTypes.string.isRequired,
|
||||
styles: PropTypes.shape({
|
||||
color: PropTypes.string,
|
||||
backgroundColor: PropTypes.string,
|
||||
}),
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
// Produces a lighter color based on a hex color and a percent
|
||||
// lightenColor("#067647", 20) will produce a color 20% lighter than #067647
|
||||
const lightenColor = (color, percent) => {
|
||||
let r = parseInt(color.substring(1, 3), 16);
|
||||
let g = parseInt(color.substring(3, 5), 16);
|
||||
let b = parseInt(color.substring(5, 7), 16);
|
||||
let r = parseInt(color.substring(1, 3), 16);
|
||||
let g = parseInt(color.substring(3, 5), 16);
|
||||
let b = parseInt(color.substring(5, 7), 16);
|
||||
|
||||
const amt = Math.round((255 * percent) / 100);
|
||||
const amt = Math.round((255 * percent) / 100);
|
||||
|
||||
r = r + amt <= 255 ? r + amt : 255;
|
||||
g = g + amt <= 255 ? g + amt : 255;
|
||||
b = b + amt <= 255 ? b + amt : 255;
|
||||
r = r + amt <= 255 ? r + amt : 255;
|
||||
g = g + amt <= 255 ? g + amt : 255;
|
||||
b = b + amt <= 255 ? b + amt : 255;
|
||||
|
||||
r = r.toString(16).padStart(2, "0");
|
||||
g = g.toString(16).padStart(2, "0");
|
||||
b = b.toString(16).padStart(2, "0");
|
||||
r = r.toString(16).padStart(2, "0");
|
||||
g = g.toString(16).padStart(2, "0");
|
||||
b = b.toString(16).padStart(2, "0");
|
||||
|
||||
return `#${r}${g}${b}`;
|
||||
return `#${r}${g}${b}`;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -84,34 +84,31 @@ const lightenColor = (color, percent) => {
|
||||
*/
|
||||
|
||||
const ColoredLabel = ({ label, color }) => {
|
||||
const theme = useTheme();
|
||||
// If an invalid color is passed, default to the labelGray color
|
||||
if (
|
||||
typeof color !== "string" ||
|
||||
!/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(color)
|
||||
) {
|
||||
color = theme.palette.border.light;
|
||||
}
|
||||
const theme = useTheme();
|
||||
// If an invalid color is passed, default to the labelGray color
|
||||
if (typeof color !== "string" || !/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(color)) {
|
||||
color = theme.palette.border.light;
|
||||
}
|
||||
|
||||
// Calculate lighter shades for border and bg
|
||||
const borderColor = lightenColor(color, 20);
|
||||
const bgColor = lightenColor(color, 75);
|
||||
// Calculate lighter shades for border and bg
|
||||
const borderColor = lightenColor(color, 20);
|
||||
const bgColor = lightenColor(color, 75);
|
||||
|
||||
return (
|
||||
<BaseLabel
|
||||
label={label}
|
||||
styles={{
|
||||
color: color,
|
||||
borderColor: borderColor,
|
||||
backgroundColor: bgColor,
|
||||
}}
|
||||
></BaseLabel>
|
||||
);
|
||||
return (
|
||||
<BaseLabel
|
||||
label={label}
|
||||
styles={{
|
||||
color: color,
|
||||
borderColor: borderColor,
|
||||
backgroundColor: bgColor,
|
||||
}}
|
||||
></BaseLabel>
|
||||
);
|
||||
};
|
||||
|
||||
ColoredLabel.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -126,69 +123,63 @@ ColoredLabel.propTypes = {
|
||||
*/
|
||||
|
||||
const StatusLabel = ({ status, text, customStyles }) => {
|
||||
const theme = useTheme();
|
||||
const colors = {
|
||||
up: {
|
||||
dotColor: theme.palette.success.main,
|
||||
bgColor: theme.palette.success.bg,
|
||||
borderColor: theme.palette.success.light,
|
||||
},
|
||||
down: {
|
||||
dotColor: theme.palette.error.text,
|
||||
bgColor: theme.palette.error.bg,
|
||||
borderColor: theme.palette.error.light,
|
||||
},
|
||||
paused: {
|
||||
dotColor: theme.palette.warning.main,
|
||||
bgColor: theme.palette.warning.bg,
|
||||
borderColor: theme.palette.warning.light,
|
||||
},
|
||||
pending: {
|
||||
dotColor: theme.palette.warning.main,
|
||||
bgColor: theme.palette.warning.bg,
|
||||
borderColor: theme.palette.warning.light,
|
||||
},
|
||||
"cannot resolve": {
|
||||
dotColor: theme.palette.unresolved.main,
|
||||
bgColor: theme.palette.unresolved.bg,
|
||||
borderColor: theme.palette.unresolved.light,
|
||||
},
|
||||
};
|
||||
const theme = useTheme();
|
||||
const colors = {
|
||||
up: {
|
||||
dotColor: theme.palette.success.main,
|
||||
bgColor: theme.palette.success.bg,
|
||||
borderColor: theme.palette.success.light,
|
||||
},
|
||||
down: {
|
||||
dotColor: theme.palette.error.text,
|
||||
bgColor: theme.palette.error.bg,
|
||||
borderColor: theme.palette.error.light,
|
||||
},
|
||||
paused: {
|
||||
dotColor: theme.palette.warning.main,
|
||||
bgColor: theme.palette.warning.bg,
|
||||
borderColor: theme.palette.warning.light,
|
||||
},
|
||||
pending: {
|
||||
dotColor: theme.palette.warning.main,
|
||||
bgColor: theme.palette.warning.bg,
|
||||
borderColor: theme.palette.warning.light,
|
||||
},
|
||||
"cannot resolve": {
|
||||
dotColor: theme.palette.unresolved.main,
|
||||
bgColor: theme.palette.unresolved.bg,
|
||||
borderColor: theme.palette.unresolved.light,
|
||||
},
|
||||
};
|
||||
|
||||
// Look up the color for the status
|
||||
const { borderColor, bgColor, dotColor } = colors[status];
|
||||
// Look up the color for the status
|
||||
const { borderColor, bgColor, dotColor } = colors[status];
|
||||
|
||||
return (
|
||||
<BaseLabel
|
||||
label={text}
|
||||
styles={{
|
||||
color: dotColor,
|
||||
backgroundColor: bgColor,
|
||||
borderColor: borderColor,
|
||||
...customStyles,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
width={7}
|
||||
height={7}
|
||||
bgcolor={dotColor}
|
||||
borderRadius="50%"
|
||||
marginRight="5px"
|
||||
/>
|
||||
</BaseLabel>
|
||||
);
|
||||
return (
|
||||
<BaseLabel
|
||||
label={text}
|
||||
styles={{
|
||||
color: dotColor,
|
||||
backgroundColor: bgColor,
|
||||
borderColor: borderColor,
|
||||
...customStyles,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
width={7}
|
||||
height={7}
|
||||
bgcolor={dotColor}
|
||||
borderRadius="50%"
|
||||
marginRight="5px"
|
||||
/>
|
||||
</BaseLabel>
|
||||
);
|
||||
};
|
||||
|
||||
StatusLabel.propTypes = {
|
||||
status: PropTypes.oneOf([
|
||||
"up",
|
||||
"down",
|
||||
"paused",
|
||||
"pending",
|
||||
"cannot resolve",
|
||||
]),
|
||||
text: PropTypes.string,
|
||||
customStyles: PropTypes.object,
|
||||
status: PropTypes.oneOf(["up", "down", "paused", "pending", "cannot resolve"]),
|
||||
text: PropTypes.string,
|
||||
customStyles: PropTypes.object,
|
||||
};
|
||||
|
||||
export { ColoredLabel, StatusLabel };
|
||||
|
||||
@@ -11,52 +11,52 @@ import PropTypes from "prop-types";
|
||||
*/
|
||||
|
||||
const Link = ({ level, label, url }) => {
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
|
||||
const levelConfig = {
|
||||
primary: {},
|
||||
secondary: {
|
||||
color: theme.palette.text.secondary,
|
||||
sx: {
|
||||
":hover": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
},
|
||||
},
|
||||
tertiary: {
|
||||
color: theme.palette.text.tertiary,
|
||||
sx: {
|
||||
textDecoration: "underline",
|
||||
textDecorationStyle: "dashed",
|
||||
textDecorationColor: theme.palette.primary.main,
|
||||
textUnderlineOffset: "1px",
|
||||
":hover": {
|
||||
color: theme.palette.text.tertiary,
|
||||
textDecorationColor: theme.palette.primary.main,
|
||||
backgroundColor: theme.palette.background.fill,
|
||||
},
|
||||
},
|
||||
},
|
||||
error: {},
|
||||
};
|
||||
const { sx, color } = levelConfig[level];
|
||||
return (
|
||||
<MuiLink
|
||||
href={url}
|
||||
sx={{ width: "fit-content", ...sx }}
|
||||
color={color}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{label}
|
||||
</MuiLink>
|
||||
);
|
||||
const levelConfig = {
|
||||
primary: {},
|
||||
secondary: {
|
||||
color: theme.palette.text.secondary,
|
||||
sx: {
|
||||
":hover": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
},
|
||||
},
|
||||
tertiary: {
|
||||
color: theme.palette.text.tertiary,
|
||||
sx: {
|
||||
textDecoration: "underline",
|
||||
textDecorationStyle: "dashed",
|
||||
textDecorationColor: theme.palette.primary.main,
|
||||
textUnderlineOffset: "1px",
|
||||
":hover": {
|
||||
color: theme.palette.text.tertiary,
|
||||
textDecorationColor: theme.palette.primary.main,
|
||||
backgroundColor: theme.palette.background.fill,
|
||||
},
|
||||
},
|
||||
},
|
||||
error: {},
|
||||
};
|
||||
const { sx, color } = levelConfig[level];
|
||||
return (
|
||||
<MuiLink
|
||||
href={url}
|
||||
sx={{ width: "fit-content", ...sx }}
|
||||
color={color}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{label}
|
||||
</MuiLink>
|
||||
);
|
||||
};
|
||||
|
||||
Link.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
level: PropTypes.oneOf(["primary", "secondary", "tertiary", "error"]),
|
||||
label: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
level: PropTypes.oneOf(["primary", "secondary", "tertiary", "error"]),
|
||||
label: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Link;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
.progress-bar-container h2.MuiTypography-root,
|
||||
.progress-bar-container p.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.progress-bar-container p.MuiTypography-root:has(span) {
|
||||
font-size: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.progress-bar-container p.MuiTypography-root span {
|
||||
padding-left: 2px;
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import React from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
Box,
|
||||
IconButton,
|
||||
LinearProgress,
|
||||
Stack,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { Box, IconButton, LinearProgress, Stack, Typography } from "@mui/material";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
|
||||
import "./index.css";
|
||||
@@ -23,161 +17,163 @@ import "./index.css";
|
||||
* @returns {JSX.Element} The rendered component.
|
||||
*/
|
||||
|
||||
const ProgressUpload = ({
|
||||
icon,
|
||||
label,
|
||||
size,
|
||||
progress = 0,
|
||||
onClick,
|
||||
error,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="progress-bar-container"
|
||||
mt={theme.spacing(10)}
|
||||
p={theme.spacing(8)}
|
||||
sx={{
|
||||
minWidth: "200px",
|
||||
height: "fit-content",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
backgroundColor: theme.palette.background.fill,
|
||||
"&:has(.input-error)": {
|
||||
borderColor: theme.palette.error.main,
|
||||
backgroundColor: theme.palette.error.bg,
|
||||
py: theme.spacing(4),
|
||||
px: theme.spacing(8),
|
||||
"& > .MuiStack-root > svg": {
|
||||
fill: theme.palette.error.text,
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
mb={error ? 0 : theme.spacing(5)}
|
||||
gap={theme.spacing(5)}
|
||||
alignItems={error ? "center" : "flex-start"}
|
||||
>
|
||||
{error ? (
|
||||
<ErrorOutlineOutlinedIcon />
|
||||
) : icon ? (
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
height: 30,
|
||||
minWidth: 30,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.dark,
|
||||
borderRadius: 2,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
"& svg": {
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 23,
|
||||
height: 23,
|
||||
"& path": {
|
||||
fill: theme.palette.other.icon,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</Box>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{error ? (
|
||||
<Typography
|
||||
component="p"
|
||||
className="input-error"
|
||||
color={theme.palette.error.text}
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
) : (
|
||||
<Box color={theme.palette.text.tertiary}>
|
||||
<Typography component="h2" mb={theme.spacing(1.5)}>
|
||||
{error ? error : label}
|
||||
</Typography>
|
||||
<Typography component="p" sx={{ opacity: 0.6 }}>
|
||||
{!error && size}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
sx={
|
||||
!error
|
||||
? {
|
||||
alignSelf: "flex-start",
|
||||
ml: "auto",
|
||||
mr: theme.spacing(-2.5),
|
||||
mt: theme.spacing(-2.5),
|
||||
padding: theme.spacing(2.5),
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}
|
||||
: {
|
||||
ml: "auto",
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<CloseIcon
|
||||
sx={{
|
||||
fontSize: "20px",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
{!error ? (
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Box sx={{ width: "100%", mr: theme.spacing(5) }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress}
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "10px",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
maxWidth: "500px",
|
||||
backgroundColor: theme.palette.border.light,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Typography
|
||||
component="p"
|
||||
sx={{ minWidth: "max-content", opacity: 0.6 }}
|
||||
>
|
||||
{progress}
|
||||
<span>%</span>
|
||||
</Typography>
|
||||
</Stack>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
return (
|
||||
<Box
|
||||
className="progress-bar-container"
|
||||
mt={theme.spacing(10)}
|
||||
p={theme.spacing(8)}
|
||||
sx={{
|
||||
minWidth: "200px",
|
||||
height: "fit-content",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
backgroundColor: theme.palette.background.fill,
|
||||
"&:has(.input-error)": {
|
||||
borderColor: theme.palette.error.main,
|
||||
backgroundColor: theme.palette.error.bg,
|
||||
py: theme.spacing(4),
|
||||
px: theme.spacing(8),
|
||||
"& > .MuiStack-root > svg": {
|
||||
fill: theme.palette.error.text,
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
mb={error ? 0 : theme.spacing(5)}
|
||||
gap={theme.spacing(5)}
|
||||
alignItems={error ? "center" : "flex-start"}
|
||||
>
|
||||
{error ? (
|
||||
<ErrorOutlineOutlinedIcon />
|
||||
) : icon ? (
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
height: 30,
|
||||
minWidth: 30,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.dark,
|
||||
borderRadius: 2,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
"& svg": {
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 23,
|
||||
height: 23,
|
||||
"& path": {
|
||||
fill: theme.palette.other.icon,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</Box>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{error ? (
|
||||
<Typography
|
||||
component="p"
|
||||
className="input-error"
|
||||
color={theme.palette.error.text}
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
) : (
|
||||
<Box color={theme.palette.text.tertiary}>
|
||||
<Typography
|
||||
component="h2"
|
||||
mb={theme.spacing(1.5)}
|
||||
>
|
||||
{error ? error : label}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="p"
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
{!error && size}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
sx={
|
||||
!error
|
||||
? {
|
||||
alignSelf: "flex-start",
|
||||
ml: "auto",
|
||||
mr: theme.spacing(-2.5),
|
||||
mt: theme.spacing(-2.5),
|
||||
padding: theme.spacing(2.5),
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}
|
||||
: {
|
||||
ml: "auto",
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<CloseIcon
|
||||
sx={{
|
||||
fontSize: "20px",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
{!error ? (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
>
|
||||
<Box sx={{ width: "100%", mr: theme.spacing(5) }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress}
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "10px",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
maxWidth: "500px",
|
||||
backgroundColor: theme.palette.border.light,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Typography
|
||||
component="p"
|
||||
sx={{ minWidth: "max-content", opacity: 0.6 }}
|
||||
>
|
||||
{progress}
|
||||
<span>%</span>
|
||||
</Typography>
|
||||
</Stack>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
ProgressUpload.propTypes = {
|
||||
icon: PropTypes.element, // JSX element for the icon (optional)
|
||||
label: PropTypes.string.isRequired, // Label text for the progress item
|
||||
size: PropTypes.string.isRequired, // Size information for the progress item
|
||||
progress: PropTypes.number.isRequired, // Current progress value (0-100)
|
||||
onClick: PropTypes.func.isRequired, // Function to handle click events on the remove button
|
||||
error: PropTypes.string, // Error message to display if there's an error (optional)
|
||||
icon: PropTypes.element, // JSX element for the icon (optional)
|
||||
label: PropTypes.string.isRequired, // Label text for the progress item
|
||||
size: PropTypes.string.isRequired, // Size information for the progress item
|
||||
progress: PropTypes.number.isRequired, // Current progress value (0-100)
|
||||
onClick: PropTypes.func.isRequired, // Function to handle click events on the remove button
|
||||
error: PropTypes.string, // Error message to display if there's an error (optional)
|
||||
};
|
||||
|
||||
export default ProgressUpload;
|
||||
|
||||
@@ -5,17 +5,17 @@ import CheckCircle from "@mui/icons-material/CheckCircle";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const CustomStepIcon = (props) => {
|
||||
const { completed, active } = props;
|
||||
return completed ? (
|
||||
<CheckCircle color="primary" />
|
||||
) : (
|
||||
<RadioButtonCheckedIcon color={active ? "primary" : "disabled"} />
|
||||
);
|
||||
const { completed, active } = props;
|
||||
return completed ? (
|
||||
<CheckCircle color="primary" />
|
||||
) : (
|
||||
<RadioButtonCheckedIcon color={active ? "primary" : "disabled"} />
|
||||
);
|
||||
};
|
||||
|
||||
CustomStepIcon.propTypes = {
|
||||
completed: PropTypes.bool.isRequired,
|
||||
active: PropTypes.bool.isRequired,
|
||||
completed: PropTypes.bool.isRequired,
|
||||
active: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -25,34 +25,43 @@ CustomStepIcon.propTypes = {
|
||||
*/
|
||||
|
||||
const ProgressStepper = ({ steps }) => {
|
||||
const [activeStep, setActiveStep] = React.useState(1);
|
||||
return (
|
||||
<Stepper activeStep={activeStep} alternativeLabel>
|
||||
{steps.map((step, index) => {
|
||||
const color = activeStep === index ? "primary" : "inherit";
|
||||
return (
|
||||
<Step key={step.label} onClick={() => setActiveStep(index)}>
|
||||
<StepLabel StepIconComponent={CustomStepIcon}>
|
||||
<Typography
|
||||
variant="body1"
|
||||
color={color}
|
||||
sx={{ fontWeight: "bold" }}
|
||||
>
|
||||
{step.label}
|
||||
</Typography>
|
||||
</StepLabel>
|
||||
<Typography variant="body1" color={color}>
|
||||
{step.content}
|
||||
</Typography>
|
||||
</Step>
|
||||
);
|
||||
})}
|
||||
</Stepper>
|
||||
);
|
||||
const [activeStep, setActiveStep] = React.useState(1);
|
||||
return (
|
||||
<Stepper
|
||||
activeStep={activeStep}
|
||||
alternativeLabel
|
||||
>
|
||||
{steps.map((step, index) => {
|
||||
const color = activeStep === index ? "primary" : "inherit";
|
||||
return (
|
||||
<Step
|
||||
key={step.label}
|
||||
onClick={() => setActiveStep(index)}
|
||||
>
|
||||
<StepLabel StepIconComponent={CustomStepIcon}>
|
||||
<Typography
|
||||
variant="body1"
|
||||
color={color}
|
||||
sx={{ fontWeight: "bold" }}
|
||||
>
|
||||
{step.label}
|
||||
</Typography>
|
||||
</StepLabel>
|
||||
<Typography
|
||||
variant="body1"
|
||||
color={color}
|
||||
>
|
||||
{step.content}
|
||||
</Typography>
|
||||
</Step>
|
||||
);
|
||||
})}
|
||||
</Stepper>
|
||||
);
|
||||
};
|
||||
|
||||
ProgressStepper.propTypes = {
|
||||
steps: PropTypes.array.isRequired,
|
||||
steps: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
export default ProgressStepper;
|
||||
|
||||
@@ -14,17 +14,20 @@ import PropTypes from "prop-types";
|
||||
*/
|
||||
|
||||
const ProtectedRoute = ({ Component, ...rest }) => {
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const authState = useSelector((state) => state.auth);
|
||||
|
||||
return authState.authToken ? (
|
||||
<Component {...rest} />
|
||||
) : (
|
||||
<Navigate to="/login" replace />
|
||||
);
|
||||
return authState.authToken ? (
|
||||
<Component {...rest} />
|
||||
) : (
|
||||
<Navigate
|
||||
to="/login"
|
||||
replace
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ProtectedRoute.propTypes = {
|
||||
Component: PropTypes.elementType.isRequired,
|
||||
Component: PropTypes.elementType.isRequired,
|
||||
};
|
||||
|
||||
export default ProtectedRoute;
|
||||
|
||||
@@ -1,94 +1,94 @@
|
||||
aside .MuiList-root svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
opacity: 0.9;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
aside span.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
line-height: 1;
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
line-height: 1;
|
||||
}
|
||||
aside .MuiStack-root + span.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium-plus);
|
||||
font-size: var(--env-var-font-size-medium-plus);
|
||||
}
|
||||
aside .MuiListSubheader-root {
|
||||
font-size: var(--env-var-font-size-small);
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 2px;
|
||||
opacity: 0.6;
|
||||
font-size: var(--env-var-font-size-small);
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 2px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
aside p.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-small);
|
||||
opacity: 0.8;
|
||||
font-size: var(--env-var-font-size-small);
|
||||
opacity: 0.8;
|
||||
}
|
||||
aside .MuiListItemButton-root:not(.selected-path) > * {
|
||||
opacity: 0.9;
|
||||
opacity: 0.9;
|
||||
}
|
||||
aside .selected-path > * {
|
||||
opacity: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
aside .selected-path span.MuiTypography-root {
|
||||
font-weight: 600;
|
||||
font-weight: 600;
|
||||
}
|
||||
aside .MuiCollapse-wrapperInner .MuiList-root > .MuiListItemButton-root {
|
||||
position: relative;
|
||||
position: relative;
|
||||
}
|
||||
aside .MuiCollapse-wrapperInner .MuiList-root svg,
|
||||
aside .MuiList-root .MuiListItemText-root + svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.sidebar-popup li.MuiButtonBase-root:has(.MuiBox-root) {
|
||||
padding-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.sidebar-popup svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 0.9;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* TRANSITIONS */
|
||||
aside {
|
||||
flex: 1;
|
||||
transition: max-width 650ms cubic-bezier(0.36, -0.01, 0, 0.77);
|
||||
flex: 1;
|
||||
transition: max-width 650ms cubic-bezier(0.36, -0.01, 0, 0.77);
|
||||
}
|
||||
.home-layout aside.collapsed {
|
||||
max-width: 64px;
|
||||
max-width: 64px;
|
||||
}
|
||||
|
||||
aside.expanded .MuiTypography-root,
|
||||
aside.expanded p.MuiTypography-root,
|
||||
aside.expanded .MuiListItemText-root + svg,
|
||||
aside.expanded .MuiAvatar-root + .MuiBox-root + .MuiIconButton-root {
|
||||
visibility: visible;
|
||||
animation: fadeIn 1s ease;
|
||||
visibility: visible;
|
||||
animation: fadeIn 1s ease;
|
||||
}
|
||||
|
||||
aside.collapsed .MuiTypography-root,
|
||||
aside.collapsed p.MuiTypography-root,
|
||||
aside.collapsed .MuiListItemText-root + svg,
|
||||
aside.collapsed .MuiAvatar-root + .MuiBox-root + .MuiIconButton-root {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
aside .MuiListSubheader-root {
|
||||
transition: padding 200ms ease;
|
||||
transition: padding 200ms ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
30% {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.9;
|
||||
visibility: visible;
|
||||
}
|
||||
0% {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
30% {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.9;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,183 +17,186 @@ import { createToast } from "../../../Utils/toastUtils";
|
||||
*/
|
||||
|
||||
const PasswordPanel = () => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
//redux state
|
||||
const { authToken, isLoading } = useSelector((state) => state.auth);
|
||||
//redux state
|
||||
const { authToken, isLoading } = useSelector((state) => state.auth);
|
||||
|
||||
const idToName = {
|
||||
"edit-current-password": "password",
|
||||
"edit-new-password": "newPassword",
|
||||
"edit-confirm-password": "confirm",
|
||||
};
|
||||
const idToName = {
|
||||
"edit-current-password": "password",
|
||||
"edit-new-password": "newPassword",
|
||||
"edit-confirm-password": "confirm",
|
||||
};
|
||||
|
||||
const [localData, setLocalData] = useState({
|
||||
password: "",
|
||||
newPassword: "",
|
||||
confirm: "",
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [localData, setLocalData] = useState({
|
||||
password: "",
|
||||
newPassword: "",
|
||||
confirm: "",
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
const name = idToName[id];
|
||||
setLocalData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
const name = idToName[id];
|
||||
setLocalData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
|
||||
const validation = credentials.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false, context: { password: localData.newPassword } }
|
||||
);
|
||||
const validation = credentials.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false, context: { password: localData.newPassword } }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
|
||||
if (validation.error) {
|
||||
updatedErrors[name] = validation.error.details[0].message;
|
||||
} else {
|
||||
delete updatedErrors[name];
|
||||
}
|
||||
return updatedErrors;
|
||||
});
|
||||
};
|
||||
if (validation.error) {
|
||||
updatedErrors[name] = validation.error.details[0].message;
|
||||
} else {
|
||||
delete updatedErrors[name];
|
||||
}
|
||||
return updatedErrors;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const { error } = credentials.validate(localData, {
|
||||
abortEarly: false,
|
||||
context: { password: localData.newPassword },
|
||||
});
|
||||
const { error } = credentials.validate(localData, {
|
||||
abortEarly: false,
|
||||
context: { password: localData.newPassword },
|
||||
});
|
||||
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
} else {
|
||||
const action = await dispatch(update({ authToken, localData }));
|
||||
if (action.payload.success) {
|
||||
createToast({
|
||||
body: "Your password was changed successfully.",
|
||||
});
|
||||
setLocalData({
|
||||
password: "",
|
||||
newPassword: "",
|
||||
confirm: "",
|
||||
});
|
||||
} else {
|
||||
// TODO: Check for other errors?
|
||||
createToast({
|
||||
body: "Your password input was incorrect.",
|
||||
});
|
||||
setErrors({ password: "*" + action.payload.msg + "." });
|
||||
}
|
||||
}
|
||||
};
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
} else {
|
||||
const action = await dispatch(update({ authToken, localData }));
|
||||
if (action.payload.success) {
|
||||
createToast({
|
||||
body: "Your password was changed successfully.",
|
||||
});
|
||||
setLocalData({
|
||||
password: "",
|
||||
newPassword: "",
|
||||
confirm: "",
|
||||
});
|
||||
} else {
|
||||
// TODO: Check for other errors?
|
||||
createToast({
|
||||
body: "Your password input was incorrect.",
|
||||
});
|
||||
setErrors({ password: "*" + action.payload.msg + "." });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TabPanel
|
||||
value="password"
|
||||
sx={{
|
||||
"& h1, & input": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
component="form"
|
||||
onSubmit={handleSubmit}
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={theme.spacing(20)}
|
||||
>
|
||||
<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
|
||||
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"]]}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">New password</Typography>
|
||||
</Box>
|
||||
<Field
|
||||
type="password"
|
||||
id="edit-new-password"
|
||||
placeholder="Enter your new password"
|
||||
autoComplete="new-password"
|
||||
value={localData.newPassword}
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-new-password"]]}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">Confirm new password</Typography>
|
||||
</Box>
|
||||
<Field
|
||||
type="password"
|
||||
id="edit-confirm-password"
|
||||
placeholder="Reenter your new password"
|
||||
autoComplete="new-password"
|
||||
value={localData.confirm}
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-confirm-password"]]}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}></Box>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Alert
|
||||
variant="warning"
|
||||
body="New password must contain at least 8 characters and must have at least one uppercase letter, one number and one symbol."
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleSubmit}
|
||||
loading={isLoading}
|
||||
loadingIndicator="Saving..."
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
sx={{
|
||||
px: theme.spacing(12),
|
||||
mt: theme.spacing(20),
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</TabPanel>
|
||||
);
|
||||
return (
|
||||
<TabPanel
|
||||
value="password"
|
||||
sx={{
|
||||
"& h1, & input": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
component="form"
|
||||
onSubmit={handleSubmit}
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={theme.spacing(20)}
|
||||
>
|
||||
<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
|
||||
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"]]}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">New password</Typography>
|
||||
</Box>
|
||||
<Field
|
||||
type="password"
|
||||
id="edit-new-password"
|
||||
placeholder="Enter your new password"
|
||||
autoComplete="new-password"
|
||||
value={localData.newPassword}
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-new-password"]]}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">Confirm new password</Typography>
|
||||
</Box>
|
||||
<Field
|
||||
type="password"
|
||||
id="edit-confirm-password"
|
||||
placeholder="Reenter your new password"
|
||||
autoComplete="new-password"
|
||||
value={localData.confirm}
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-confirm-password"]]}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}></Box>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Alert
|
||||
variant="warning"
|
||||
body="New password must contain at least 8 characters and must have at least one uppercase letter, one number and one symbol."
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleSubmit}
|
||||
loading={isLoading}
|
||||
loadingIndicator="Saving..."
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
sx={{
|
||||
px: theme.spacing(12),
|
||||
mt: theme.spacing(20),
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</TabPanel>
|
||||
);
|
||||
};
|
||||
|
||||
PasswordPanel.propTypes = {
|
||||
// No props are being passed to this component, hence no specific PropTypes are defined.
|
||||
// No props are being passed to this component, hence no specific PropTypes are defined.
|
||||
};
|
||||
|
||||
export default PasswordPanel;
|
||||
|
||||
@@ -7,11 +7,7 @@ import Field from "../../Inputs/Field";
|
||||
import ImageField from "../../Inputs/Image";
|
||||
import { credentials, imageValidation } from "../../../Validation/validation";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
clearAuthState,
|
||||
deleteUser,
|
||||
update,
|
||||
} from "../../../Features/Auth/authSlice";
|
||||
import { clearAuthState, deleteUser, update } from "../../../Features/Auth/authSlice";
|
||||
import ImageIcon from "@mui/icons-material/Image";
|
||||
import ProgressUpload from "../../ProgressBars";
|
||||
import { formatBytes } from "../../../Utils/fileUtils";
|
||||
@@ -29,471 +25,508 @@ import LoadingButton from "@mui/lab/LoadingButton";
|
||||
*/
|
||||
|
||||
const ProfilePanel = () => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
//redux state
|
||||
const { user, authToken, isLoading } = useSelector((state) => state.auth);
|
||||
//redux state
|
||||
const { user, authToken, isLoading } = useSelector((state) => state.auth);
|
||||
|
||||
const idToName = {
|
||||
"edit-first-name": "firstName",
|
||||
"edit-last-name": "lastName",
|
||||
// Disabled for now, will revisit in the future
|
||||
// "edit-email": "email",
|
||||
};
|
||||
const idToName = {
|
||||
"edit-first-name": "firstName",
|
||||
"edit-last-name": "lastName",
|
||||
// Disabled for now, will revisit in the future
|
||||
// "edit-email": "email",
|
||||
};
|
||||
|
||||
// Local state for form data, errors, and file handling
|
||||
const [localData, setLocalData] = useState({
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
// email: user.email, // Disabled for now
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [file, setFile] = useState();
|
||||
const intervalRef = useRef(null);
|
||||
const [progress, setProgress] = useState({ value: 0, isLoading: false });
|
||||
// Local state for form data, errors, and file handling
|
||||
const [localData, setLocalData] = useState({
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
// email: user.email, // Disabled for now
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [file, setFile] = useState();
|
||||
const intervalRef = useRef(null);
|
||||
const [progress, setProgress] = useState({ value: 0, isLoading: false });
|
||||
|
||||
// Handles input field changes and performs validation
|
||||
const handleChange = (event) => {
|
||||
errors["unchanged"] && clearError("unchanged");
|
||||
const { value, id } = event.target;
|
||||
const name = idToName[id];
|
||||
setLocalData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
// Handles input field changes and performs validation
|
||||
const handleChange = (event) => {
|
||||
errors["unchanged"] && clearError("unchanged");
|
||||
const { value, id } = event.target;
|
||||
const name = idToName[id];
|
||||
setLocalData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
|
||||
validateField({ [name]: value }, credentials, name);
|
||||
};
|
||||
validateField({ [name]: value }, credentials, name);
|
||||
};
|
||||
|
||||
// Handles image file
|
||||
const handlePicture = (event) => {
|
||||
const pic = event.target.files[0];
|
||||
let error = validateField(
|
||||
{ type: pic.type, size: pic.size },
|
||||
imageValidation
|
||||
);
|
||||
if (error) return;
|
||||
// Handles image file
|
||||
const handlePicture = (event) => {
|
||||
const pic = event.target.files[0];
|
||||
let error = validateField({ type: pic.type, size: pic.size }, imageValidation);
|
||||
if (error) return;
|
||||
|
||||
setProgress((prev) => ({ ...prev, isLoading: true }));
|
||||
setFile({
|
||||
src: URL.createObjectURL(pic),
|
||||
name: pic.name,
|
||||
size: formatBytes(pic.size),
|
||||
delete: false,
|
||||
});
|
||||
setProgress((prev) => ({ ...prev, isLoading: true }));
|
||||
setFile({
|
||||
src: URL.createObjectURL(pic),
|
||||
name: pic.name,
|
||||
size: formatBytes(pic.size),
|
||||
delete: false,
|
||||
});
|
||||
|
||||
//TODO - potentitally remove, will revisit in the future
|
||||
intervalRef.current = setInterval(() => {
|
||||
const buffer = 12;
|
||||
setProgress((prev) => {
|
||||
if (prev.value + buffer >= 100) {
|
||||
clearInterval(intervalRef.current);
|
||||
return { value: 100, isLoading: false };
|
||||
}
|
||||
return { ...prev, value: prev.value + buffer };
|
||||
});
|
||||
}, 120);
|
||||
};
|
||||
//TODO - potentitally remove, will revisit in the future
|
||||
intervalRef.current = setInterval(() => {
|
||||
const buffer = 12;
|
||||
setProgress((prev) => {
|
||||
if (prev.value + buffer >= 100) {
|
||||
clearInterval(intervalRef.current);
|
||||
return { value: 100, isLoading: false };
|
||||
}
|
||||
return { ...prev, value: prev.value + buffer };
|
||||
});
|
||||
}, 120);
|
||||
};
|
||||
|
||||
// Validates input against provided schema and updates error state
|
||||
const validateField = (toValidate, schema, name = "picture") => {
|
||||
const { error } = schema.validate(toValidate, { abortEarly: false });
|
||||
setErrors((prev) => {
|
||||
const prevErrors = { ...prev };
|
||||
if (error) prevErrors[name] = error.details[0].message;
|
||||
else delete prevErrors[name];
|
||||
return prevErrors;
|
||||
});
|
||||
if (error) return true;
|
||||
};
|
||||
// Validates input against provided schema and updates error state
|
||||
const validateField = (toValidate, schema, name = "picture") => {
|
||||
const { error } = schema.validate(toValidate, { abortEarly: false });
|
||||
setErrors((prev) => {
|
||||
const prevErrors = { ...prev };
|
||||
if (error) prevErrors[name] = error.details[0].message;
|
||||
else delete prevErrors[name];
|
||||
return prevErrors;
|
||||
});
|
||||
if (error) return true;
|
||||
};
|
||||
|
||||
// Clears specific error from errors state
|
||||
const clearError = (err) => {
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
if (updatedErrors[err]) delete updatedErrors[err];
|
||||
return updatedErrors;
|
||||
});
|
||||
};
|
||||
// Clears specific error from errors state
|
||||
const clearError = (err) => {
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
if (updatedErrors[err]) delete updatedErrors[err];
|
||||
return updatedErrors;
|
||||
});
|
||||
};
|
||||
|
||||
// Resets picture-related states and clears interval
|
||||
const removePicture = () => {
|
||||
errors["picture"] && clearError("picture");
|
||||
setFile({ delete: true });
|
||||
clearInterval(intervalRef.current); // interrupt interval if image upload is canceled prior to completing the process
|
||||
setProgress({ value: 0, isLoading: false });
|
||||
};
|
||||
// Resets picture-related states and clears interval
|
||||
const removePicture = () => {
|
||||
errors["picture"] && clearError("picture");
|
||||
setFile({ delete: true });
|
||||
clearInterval(intervalRef.current); // interrupt interval if image upload is canceled prior to completing the process
|
||||
setProgress({ value: 0, isLoading: false });
|
||||
};
|
||||
|
||||
// Opens the picture update modal
|
||||
const openPictureModal = () => {
|
||||
setIsOpen("picture");
|
||||
setFile({ delete: localData.deleteProfileImage });
|
||||
};
|
||||
// Opens the picture update modal
|
||||
const openPictureModal = () => {
|
||||
setIsOpen("picture");
|
||||
setFile({ delete: localData.deleteProfileImage });
|
||||
};
|
||||
|
||||
// Closes the picture update modal and resets related states
|
||||
const closePictureModal = () => {
|
||||
errors["picture"] && clearError("picture");
|
||||
setFile(); //reset file
|
||||
clearInterval(intervalRef.current); // interrupt interval if image upload is canceled prior to completing the process
|
||||
setProgress({ value: 0, isLoading: false });
|
||||
setIsOpen("");
|
||||
};
|
||||
// Closes the picture update modal and resets related states
|
||||
const closePictureModal = () => {
|
||||
errors["picture"] && clearError("picture");
|
||||
setFile(); //reset file
|
||||
clearInterval(intervalRef.current); // interrupt interval if image upload is canceled prior to completing the process
|
||||
setProgress({ value: 0, isLoading: false });
|
||||
setIsOpen("");
|
||||
};
|
||||
|
||||
// Updates profile image displayed on UI
|
||||
const handleUpdatePicture = () => {
|
||||
setProgress({ value: 0, isLoading: false });
|
||||
setLocalData((prev) => ({
|
||||
...prev,
|
||||
file: file.src,
|
||||
deleteProfileImage: false,
|
||||
}));
|
||||
setIsOpen("");
|
||||
errors["unchanged"] && clearError("unchanged");
|
||||
};
|
||||
// Updates profile image displayed on UI
|
||||
const handleUpdatePicture = () => {
|
||||
setProgress({ value: 0, isLoading: false });
|
||||
setLocalData((prev) => ({
|
||||
...prev,
|
||||
file: file.src,
|
||||
deleteProfileImage: false,
|
||||
}));
|
||||
setIsOpen("");
|
||||
errors["unchanged"] && clearError("unchanged");
|
||||
};
|
||||
|
||||
// Handles form submission to update user profile
|
||||
const handleSaveProfile = async (event) => {
|
||||
event.preventDefault();
|
||||
if (
|
||||
localData.firstName === user.firstName &&
|
||||
localData.lastName === user.lastName &&
|
||||
localData.deleteProfileImage === undefined &&
|
||||
localData.file === undefined
|
||||
) {
|
||||
createToast({
|
||||
body: "Unable to update profile — no changes detected.",
|
||||
});
|
||||
setErrors({ unchanged: "unable to update profile" });
|
||||
return;
|
||||
}
|
||||
// Handles form submission to update user profile
|
||||
const handleSaveProfile = async (event) => {
|
||||
event.preventDefault();
|
||||
if (
|
||||
localData.firstName === user.firstName &&
|
||||
localData.lastName === user.lastName &&
|
||||
localData.deleteProfileImage === undefined &&
|
||||
localData.file === undefined
|
||||
) {
|
||||
createToast({
|
||||
body: "Unable to update profile — no changes detected.",
|
||||
});
|
||||
setErrors({ unchanged: "unable to update profile" });
|
||||
return;
|
||||
}
|
||||
|
||||
const action = await dispatch(update({ authToken, localData }));
|
||||
if (action.payload.success) {
|
||||
createToast({
|
||||
body: "Your profile data was changed successfully.",
|
||||
});
|
||||
} else {
|
||||
createToast({
|
||||
body: "There was an error updating your profile data.",
|
||||
});
|
||||
}
|
||||
};
|
||||
const action = await dispatch(update({ authToken, localData }));
|
||||
if (action.payload.success) {
|
||||
createToast({
|
||||
body: "Your profile data was changed successfully.",
|
||||
});
|
||||
} else {
|
||||
createToast({
|
||||
body: "There was an error updating your profile data.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Removes current profile image from UI
|
||||
const handleDeletePicture = () => {
|
||||
setLocalData((prev) => ({
|
||||
...prev,
|
||||
deleteProfileImage: true,
|
||||
}));
|
||||
errors["unchanged"] && clearError("unchanged");
|
||||
};
|
||||
// Removes current profile image from UI
|
||||
const handleDeletePicture = () => {
|
||||
setLocalData((prev) => ({
|
||||
...prev,
|
||||
deleteProfileImage: true,
|
||||
}));
|
||||
errors["unchanged"] && clearError("unchanged");
|
||||
};
|
||||
|
||||
// Initiates the account deletion process
|
||||
const handleDeleteAccount = async () => {
|
||||
const action = await dispatch(deleteUser(authToken));
|
||||
if (action.payload.success) {
|
||||
dispatch(clearAuthState());
|
||||
dispatch(clearUptimeMonitorState());
|
||||
} else {
|
||||
if (action.payload) {
|
||||
// dispatch errors
|
||||
createToast({
|
||||
body: action.payload.msg,
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
body: "Unknown error.",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
// Initiates the account deletion process
|
||||
const handleDeleteAccount = async () => {
|
||||
const action = await dispatch(deleteUser(authToken));
|
||||
if (action.payload.success) {
|
||||
dispatch(clearAuthState());
|
||||
dispatch(clearUptimeMonitorState());
|
||||
} else {
|
||||
if (action.payload) {
|
||||
// dispatch errors
|
||||
createToast({
|
||||
body: action.payload.msg,
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
body: "Unknown error.",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Modal state and control functions
|
||||
const [isOpen, setIsOpen] = useState("");
|
||||
const isModalOpen = (name) => isOpen === name;
|
||||
// Modal state and control functions
|
||||
const [isOpen, setIsOpen] = useState("");
|
||||
const isModalOpen = (name) => isOpen === name;
|
||||
|
||||
return (
|
||||
<TabPanel
|
||||
value="profile"
|
||||
sx={{
|
||||
"& h1, & p, & input": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
component="form"
|
||||
className="edit-profile-form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={theme.spacing(20)}
|
||||
>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">First name</Typography>
|
||||
</Box>
|
||||
<Field
|
||||
id="edit-first-name"
|
||||
value={localData.firstName}
|
||||
placeholder="Enter your first name"
|
||||
autoComplete="given-name"
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-first-name"]]}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">Last name</Typography>
|
||||
</Box>
|
||||
<Field
|
||||
id="edit-last-name"
|
||||
placeholder="Enter your last name"
|
||||
autoComplete="family-name"
|
||||
value={localData.lastName}
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-last-name"]]}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Stack flex={0.9}>
|
||||
<Typography component="h1">Email</Typography>
|
||||
<Typography component="p" sx={{ opacity: 0.6 }}>
|
||||
This is your current email address — it cannot be changed.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Field
|
||||
id="edit-email"
|
||||
value={user.email}
|
||||
placeholder="Enter your email"
|
||||
autoComplete="email"
|
||||
onChange={() => logger.warn("disabled")}
|
||||
// error={errors[idToName["edit-email"]]}
|
||||
disabled={true}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Stack flex={0.9}>
|
||||
<Typography component="h1">Your photo</Typography>
|
||||
<Typography component="p" sx={{ opacity: 0.6 }}>
|
||||
This photo will be displayed in your profile page.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack direction="row" alignItems="center" flex={1}>
|
||||
<Avatar
|
||||
src={
|
||||
localData?.deleteProfileImage
|
||||
? "/static/images/avatar/2.jpg"
|
||||
: localData?.file
|
||||
? localData.file
|
||||
: ""
|
||||
}
|
||||
sx={{ mr: "8px" }}
|
||||
/>
|
||||
<Button variant="text" color="info" onClick={handleDeletePicture}>
|
||||
Delete
|
||||
</Button>
|
||||
<Button variant="text" color="primary" onClick={openPictureModal}>
|
||||
Update
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Divider
|
||||
aria-hidden="true"
|
||||
width="0"
|
||||
sx={{
|
||||
marginY: theme.spacing(1),
|
||||
}}
|
||||
/>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Box width="fit-content">
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleSaveProfile}
|
||||
loading={isLoading}
|
||||
loadingIndicator="Saving..."
|
||||
disabled={
|
||||
Object.keys(errors).length !== 0 && !errors?.picture && true
|
||||
}
|
||||
sx={{ px: theme.spacing(12) }}
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Divider
|
||||
aria-hidden="true"
|
||||
sx={{
|
||||
marginY: theme.spacing(20),
|
||||
borderColor: theme.palette.border.light,
|
||||
}}
|
||||
/>
|
||||
{!user.role.includes("demo") && (
|
||||
<Box component="form" noValidate spellCheck="false">
|
||||
<Box mb={theme.spacing(6)}>
|
||||
<Typography component="h1">Delete account</Typography>
|
||||
<Typography component="p" sx={{ opacity: 0.6 }}>
|
||||
Note that deleting your account will remove all data from our
|
||||
system. This is permanent and non-recoverable.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => setIsOpen("delete")}
|
||||
>
|
||||
Delete account
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
{/* TODO - Update ModalPopup Component with @mui for reusability */}
|
||||
<Modal
|
||||
aria-labelledby="modal-delete-account"
|
||||
aria-describedby="delete-account-confirmation"
|
||||
open={isModalOpen("delete")}
|
||||
onClose={() => setIsOpen("")}
|
||||
disablePortal
|
||||
>
|
||||
<Stack
|
||||
gap={theme.spacing(5)}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 500,
|
||||
bgcolor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxShadow: 24,
|
||||
p: theme.spacing(15),
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography id="modal-delete-account" component="h1">
|
||||
Really delete this account?
|
||||
</Typography>
|
||||
<Typography id="delete-account-confirmation" component="p">
|
||||
If you delete your account, you will no longer be able to sign in,
|
||||
and all of your data will be deleted. Deleting your account is
|
||||
permanent and non-recoverable action.
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(5)}
|
||||
mt={theme.spacing(5)}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button variant="text" color="info" onClick={() => setIsOpen("")}>
|
||||
Cancel
|
||||
</Button>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={handleDeleteAccount}
|
||||
loading={isLoading}
|
||||
>
|
||||
Delete account
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
<Modal
|
||||
aria-labelledby="modal-update-picture"
|
||||
aria-describedby="update-profile-picture"
|
||||
open={isModalOpen("picture")}
|
||||
onClose={closePictureModal}
|
||||
>
|
||||
<Stack
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 475,
|
||||
bgcolor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxShadow: theme.shape.boxShadow,
|
||||
p: theme.spacing(15),
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
id="modal-update-picture"
|
||||
component="h1"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
Upload Image
|
||||
</Typography>
|
||||
<ImageField
|
||||
id="update-profile-picture"
|
||||
src={
|
||||
file?.delete
|
||||
? ""
|
||||
: file?.src
|
||||
? file.src
|
||||
: localData?.file
|
||||
? localData.file
|
||||
: user?.avatarImage
|
||||
? `data:image/png;base64,${user.avatarImage}`
|
||||
: ""
|
||||
}
|
||||
loading={progress.isLoading && progress.value !== 100}
|
||||
onChange={handlePicture}
|
||||
/>
|
||||
{progress.isLoading || progress.value !== 0 || errors["picture"] ? (
|
||||
<ProgressUpload
|
||||
icon={<ImageIcon />}
|
||||
label={file?.name}
|
||||
size={file?.size}
|
||||
progress={progress.value}
|
||||
onClick={removePicture}
|
||||
error={errors["picture"]}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<Stack
|
||||
direction="row"
|
||||
mt={theme.spacing(10)}
|
||||
gap={theme.spacing(5)}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button variant="text" color="info" onClick={removePicture}>
|
||||
Remove
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleUpdatePicture}
|
||||
disabled={
|
||||
(Object.keys(errors).length !== 0 && errors?.picture) ||
|
||||
progress.value !== 100
|
||||
? true
|
||||
: false
|
||||
}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</TabPanel>
|
||||
);
|
||||
return (
|
||||
<TabPanel
|
||||
value="profile"
|
||||
sx={{
|
||||
"& h1, & p, & input": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
component="form"
|
||||
className="edit-profile-form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={theme.spacing(20)}
|
||||
>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">First name</Typography>
|
||||
</Box>
|
||||
<Field
|
||||
id="edit-first-name"
|
||||
value={localData.firstName}
|
||||
placeholder="Enter your first name"
|
||||
autoComplete="given-name"
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-first-name"]]}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">Last name</Typography>
|
||||
</Box>
|
||||
<Field
|
||||
id="edit-last-name"
|
||||
placeholder="Enter your last name"
|
||||
autoComplete="family-name"
|
||||
value={localData.lastName}
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-last-name"]]}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Stack flex={0.9}>
|
||||
<Typography component="h1">Email</Typography>
|
||||
<Typography
|
||||
component="p"
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
This is your current email address — it cannot be changed.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Field
|
||||
id="edit-email"
|
||||
value={user.email}
|
||||
placeholder="Enter your email"
|
||||
autoComplete="email"
|
||||
onChange={() => logger.warn("disabled")}
|
||||
// error={errors[idToName["edit-email"]]}
|
||||
disabled={true}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Stack flex={0.9}>
|
||||
<Typography component="h1">Your photo</Typography>
|
||||
<Typography
|
||||
component="p"
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
This photo will be displayed in your profile page.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
flex={1}
|
||||
>
|
||||
<Avatar
|
||||
src={
|
||||
localData?.deleteProfileImage
|
||||
? "/static/images/avatar/2.jpg"
|
||||
: localData?.file
|
||||
? localData.file
|
||||
: ""
|
||||
}
|
||||
sx={{ mr: "8px" }}
|
||||
/>
|
||||
<Button
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={handleDeletePicture}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<Button
|
||||
variant="text"
|
||||
color="primary"
|
||||
onClick={openPictureModal}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Divider
|
||||
aria-hidden="true"
|
||||
width="0"
|
||||
sx={{
|
||||
marginY: theme.spacing(1),
|
||||
}}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Box width="fit-content">
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleSaveProfile}
|
||||
loading={isLoading}
|
||||
loadingIndicator="Saving..."
|
||||
disabled={Object.keys(errors).length !== 0 && !errors?.picture && true}
|
||||
sx={{ px: theme.spacing(12) }}
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Divider
|
||||
aria-hidden="true"
|
||||
sx={{
|
||||
marginY: theme.spacing(20),
|
||||
borderColor: theme.palette.border.light,
|
||||
}}
|
||||
/>
|
||||
{!user.role.includes("demo") && (
|
||||
<Box
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
>
|
||||
<Box mb={theme.spacing(6)}>
|
||||
<Typography component="h1">Delete account</Typography>
|
||||
<Typography
|
||||
component="p"
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
Note that deleting your account will remove all data from our system. This
|
||||
is permanent and non-recoverable.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => setIsOpen("delete")}
|
||||
>
|
||||
Delete account
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
{/* TODO - Update ModalPopup Component with @mui for reusability */}
|
||||
<Modal
|
||||
aria-labelledby="modal-delete-account"
|
||||
aria-describedby="delete-account-confirmation"
|
||||
open={isModalOpen("delete")}
|
||||
onClose={() => setIsOpen("")}
|
||||
disablePortal
|
||||
>
|
||||
<Stack
|
||||
gap={theme.spacing(5)}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 500,
|
||||
bgcolor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxShadow: 24,
|
||||
p: theme.spacing(15),
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
id="modal-delete-account"
|
||||
component="h1"
|
||||
>
|
||||
Really delete this account?
|
||||
</Typography>
|
||||
<Typography
|
||||
id="delete-account-confirmation"
|
||||
component="p"
|
||||
>
|
||||
If you delete your account, you will no longer be able to sign in, and all of
|
||||
your data will be deleted. Deleting your account is permanent and
|
||||
non-recoverable action.
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(5)}
|
||||
mt={theme.spacing(5)}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={() => setIsOpen("")}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={handleDeleteAccount}
|
||||
loading={isLoading}
|
||||
>
|
||||
Delete account
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
<Modal
|
||||
aria-labelledby="modal-update-picture"
|
||||
aria-describedby="update-profile-picture"
|
||||
open={isModalOpen("picture")}
|
||||
onClose={closePictureModal}
|
||||
>
|
||||
<Stack
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 475,
|
||||
bgcolor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxShadow: theme.shape.boxShadow,
|
||||
p: theme.spacing(15),
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
id="modal-update-picture"
|
||||
component="h1"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
Upload Image
|
||||
</Typography>
|
||||
<ImageField
|
||||
id="update-profile-picture"
|
||||
src={
|
||||
file?.delete
|
||||
? ""
|
||||
: file?.src
|
||||
? file.src
|
||||
: localData?.file
|
||||
? localData.file
|
||||
: user?.avatarImage
|
||||
? `data:image/png;base64,${user.avatarImage}`
|
||||
: ""
|
||||
}
|
||||
loading={progress.isLoading && progress.value !== 100}
|
||||
onChange={handlePicture}
|
||||
/>
|
||||
{progress.isLoading || progress.value !== 0 || errors["picture"] ? (
|
||||
<ProgressUpload
|
||||
icon={<ImageIcon />}
|
||||
label={file?.name}
|
||||
size={file?.size}
|
||||
progress={progress.value}
|
||||
onClick={removePicture}
|
||||
error={errors["picture"]}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<Stack
|
||||
direction="row"
|
||||
mt={theme.spacing(10)}
|
||||
gap={theme.spacing(5)}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={removePicture}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleUpdatePicture}
|
||||
disabled={
|
||||
(Object.keys(errors).length !== 0 && errors?.picture) ||
|
||||
progress.value !== 100
|
||||
? true
|
||||
: false
|
||||
}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</TabPanel>
|
||||
);
|
||||
};
|
||||
|
||||
ProfilePanel.propTypes = {
|
||||
// No props are being passed to this component, hence no specific PropTypes are defined.
|
||||
// No props are being passed to this component, hence no specific PropTypes are defined.
|
||||
};
|
||||
|
||||
export default ProfilePanel;
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import TabPanel from "@mui/lab/TabPanel";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Modal,
|
||||
Stack,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { Box, Button, ButtonGroup, Modal, Stack, Typography } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import Field from "../../Inputs/Field";
|
||||
import { credentials } from "../../../Validation/validation";
|
||||
@@ -27,216 +20,211 @@ import LoadingButton from "@mui/lab/LoadingButton";
|
||||
*/
|
||||
|
||||
const TeamPanel = () => {
|
||||
const roleMap = {
|
||||
superadmin: "Super admin",
|
||||
admin: "Admin",
|
||||
user: "Team member",
|
||||
demo: "Demo User",
|
||||
};
|
||||
const roleMap = {
|
||||
superadmin: "Super admin",
|
||||
admin: "Admin",
|
||||
user: "Team member",
|
||||
demo: "Demo User",
|
||||
};
|
||||
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
|
||||
const { authToken, user } = useSelector((state) => state.auth);
|
||||
//TODO
|
||||
const [orgStates, setOrgStates] = useState({
|
||||
name: "Bluewave Labs",
|
||||
isEdit: false,
|
||||
});
|
||||
const [toInvite, setToInvite] = useState({
|
||||
email: "",
|
||||
role: ["0"],
|
||||
});
|
||||
const [tableData, setTableData] = useState({});
|
||||
const [members, setMembers] = useState([]);
|
||||
const [filter, setFilter] = useState("all");
|
||||
const [isDisabled, setIsDisabled] = useState(true);
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isSendingInvite, setIsSendingInvite] = useState(false);
|
||||
const { authToken, user } = useSelector((state) => state.auth);
|
||||
//TODO
|
||||
const [orgStates, setOrgStates] = useState({
|
||||
name: "Bluewave Labs",
|
||||
isEdit: false,
|
||||
});
|
||||
const [toInvite, setToInvite] = useState({
|
||||
email: "",
|
||||
role: ["0"],
|
||||
});
|
||||
const [tableData, setTableData] = useState({});
|
||||
const [members, setMembers] = useState([]);
|
||||
const [filter, setFilter] = useState("all");
|
||||
const [isDisabled, setIsDisabled] = useState(true);
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isSendingInvite, setIsSendingInvite] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTeam = async () => {
|
||||
try {
|
||||
const response = await networkService.getAllUsers({
|
||||
authToken: authToken,
|
||||
});
|
||||
setMembers(response.data.data);
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: error.message || "Error fetching team members.",
|
||||
});
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
const fetchTeam = async () => {
|
||||
try {
|
||||
const response = await networkService.getAllUsers({
|
||||
authToken: authToken,
|
||||
});
|
||||
setMembers(response.data.data);
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: error.message || "Error fetching team members.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
fetchTeam();
|
||||
}, [user]);
|
||||
fetchTeam();
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
let team = members;
|
||||
if (filter !== "all")
|
||||
team = members.filter((member) => {
|
||||
if (filter === "admin") {
|
||||
return (
|
||||
member.role.includes("admin") || member.role.includes("superadmin")
|
||||
);
|
||||
}
|
||||
return member.role.includes(filter);
|
||||
});
|
||||
useEffect(() => {
|
||||
let team = members;
|
||||
if (filter !== "all")
|
||||
team = members.filter((member) => {
|
||||
if (filter === "admin") {
|
||||
return member.role.includes("admin") || member.role.includes("superadmin");
|
||||
}
|
||||
return member.role.includes(filter);
|
||||
});
|
||||
|
||||
const data = {
|
||||
cols: [
|
||||
{ id: 1, name: "NAME" },
|
||||
{ id: 2, name: "EMAIL" },
|
||||
{ id: 3, name: "ROLE" },
|
||||
// FEATURE STILL TO BE IMPLEMENTED
|
||||
// { id: 4, name: "ACTION" },
|
||||
],
|
||||
rows: team?.map((member, idx) => {
|
||||
const roles = member.role.map((role) => roleMap[role]).join(",");
|
||||
return {
|
||||
id: member._id,
|
||||
data: [
|
||||
{
|
||||
id: idx,
|
||||
data: (
|
||||
<Stack>
|
||||
<Typography color={theme.palette.text.secondary}>
|
||||
{member.firstName + " " + member.lastName}
|
||||
</Typography>
|
||||
<Typography>
|
||||
Created {new Date(member.createdAt).toLocaleDateString()}
|
||||
</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{ id: idx + 1, data: member.email },
|
||||
{
|
||||
// TODO - Add select dropdown
|
||||
id: idx + 2,
|
||||
data: roles,
|
||||
},
|
||||
// FEATURE STILL TO BE IMPLEMENTED
|
||||
// {
|
||||
// // TODO - Add delete onClick
|
||||
// id: idx + 3,
|
||||
// data: (
|
||||
// <IconButton
|
||||
// aria-label="remove member"
|
||||
// sx={{
|
||||
// "&:focus": {
|
||||
// outline: "none",
|
||||
// },
|
||||
// }}
|
||||
// >
|
||||
// <Remove />
|
||||
// </IconButton>
|
||||
// ),
|
||||
// },
|
||||
],
|
||||
};
|
||||
}),
|
||||
};
|
||||
const data = {
|
||||
cols: [
|
||||
{ id: 1, name: "NAME" },
|
||||
{ id: 2, name: "EMAIL" },
|
||||
{ id: 3, name: "ROLE" },
|
||||
// FEATURE STILL TO BE IMPLEMENTED
|
||||
// { id: 4, name: "ACTION" },
|
||||
],
|
||||
rows: team?.map((member, idx) => {
|
||||
const roles = member.role.map((role) => roleMap[role]).join(",");
|
||||
return {
|
||||
id: member._id,
|
||||
data: [
|
||||
{
|
||||
id: idx,
|
||||
data: (
|
||||
<Stack>
|
||||
<Typography color={theme.palette.text.secondary}>
|
||||
{member.firstName + " " + member.lastName}
|
||||
</Typography>
|
||||
<Typography>
|
||||
Created {new Date(member.createdAt).toLocaleDateString()}
|
||||
</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{ id: idx + 1, data: member.email },
|
||||
{
|
||||
// TODO - Add select dropdown
|
||||
id: idx + 2,
|
||||
data: roles,
|
||||
},
|
||||
// FEATURE STILL TO BE IMPLEMENTED
|
||||
// {
|
||||
// // TODO - Add delete onClick
|
||||
// id: idx + 3,
|
||||
// data: (
|
||||
// <IconButton
|
||||
// aria-label="remove member"
|
||||
// sx={{
|
||||
// "&:focus": {
|
||||
// outline: "none",
|
||||
// },
|
||||
// }}
|
||||
// >
|
||||
// <Remove />
|
||||
// </IconButton>
|
||||
// ),
|
||||
// },
|
||||
],
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
setTableData(data);
|
||||
}, [members, filter]);
|
||||
useEffect(() => {
|
||||
setIsDisabled(Object.keys(errors).length !== 0 || toInvite.email === "");
|
||||
}, [errors, toInvite.email]);
|
||||
setTableData(data);
|
||||
}, [members, filter]);
|
||||
useEffect(() => {
|
||||
setIsDisabled(Object.keys(errors).length !== 0 || toInvite.email === "");
|
||||
}, [errors, toInvite.email]);
|
||||
|
||||
// RENAME ORGANIZATION
|
||||
const toggleEdit = () => {
|
||||
setOrgStates((prev) => ({ ...prev, isEdit: !prev.isEdit }));
|
||||
};
|
||||
const handleRename = () => {};
|
||||
// RENAME ORGANIZATION
|
||||
const toggleEdit = () => {
|
||||
setOrgStates((prev) => ({ ...prev, isEdit: !prev.isEdit }));
|
||||
};
|
||||
const handleRename = () => {};
|
||||
|
||||
// INVITE MEMBER
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// INVITE MEMBER
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value } = event.target;
|
||||
setToInvite((prev) => ({
|
||||
...prev,
|
||||
email: value,
|
||||
}));
|
||||
const handleChange = (event) => {
|
||||
const { value } = event.target;
|
||||
setToInvite((prev) => ({
|
||||
...prev,
|
||||
email: value,
|
||||
}));
|
||||
|
||||
const validation = credentials.validate(
|
||||
{ email: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
const validation = credentials.validate({ email: value }, { abortEarly: false });
|
||||
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
|
||||
if (validation.error) {
|
||||
updatedErrors.email = validation.error.details[0].message;
|
||||
} else {
|
||||
delete updatedErrors.email;
|
||||
}
|
||||
return updatedErrors;
|
||||
});
|
||||
};
|
||||
if (validation.error) {
|
||||
updatedErrors.email = validation.error.details[0].message;
|
||||
} else {
|
||||
delete updatedErrors.email;
|
||||
}
|
||||
return updatedErrors;
|
||||
});
|
||||
};
|
||||
|
||||
const handleInviteMember = async () => {
|
||||
if (!toInvite.email) {
|
||||
setErrors((prev) => ({ ...prev, email: "Email is required." }));
|
||||
return;
|
||||
}
|
||||
setIsSendingInvite(true);
|
||||
if (!toInvite.role.includes("user") || !toInvite.role.includes("admin"))
|
||||
setToInvite((prev) => ({ ...prev, role: ["user"] }));
|
||||
const handleInviteMember = async () => {
|
||||
if (!toInvite.email) {
|
||||
setErrors((prev) => ({ ...prev, email: "Email is required." }));
|
||||
return;
|
||||
}
|
||||
setIsSendingInvite(true);
|
||||
if (!toInvite.role.includes("user") || !toInvite.role.includes("admin"))
|
||||
setToInvite((prev) => ({ ...prev, role: ["user"] }));
|
||||
|
||||
const { error } = credentials.validate(
|
||||
{ email: toInvite.email },
|
||||
{
|
||||
abortEarly: false,
|
||||
}
|
||||
);
|
||||
const { error } = credentials.validate(
|
||||
{ email: toInvite.email },
|
||||
{
|
||||
abortEarly: false,
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
setErrors((prev) => ({ ...prev, email: error.details[0].message }));
|
||||
return;
|
||||
}
|
||||
if (error) {
|
||||
setErrors((prev) => ({ ...prev, email: error.details[0].message }));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await networkService.requestInvitationToken({
|
||||
authToken: authToken,
|
||||
email: toInvite.email,
|
||||
role: toInvite.role,
|
||||
});
|
||||
closeInviteModal();
|
||||
createToast({
|
||||
body: "Member invited. They will receive an email with details on how to create their account.",
|
||||
});
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: error.message || "Unknown error.",
|
||||
});
|
||||
} finally {
|
||||
setIsSendingInvite(false);
|
||||
}
|
||||
};
|
||||
try {
|
||||
await networkService.requestInvitationToken({
|
||||
authToken: authToken,
|
||||
email: toInvite.email,
|
||||
role: toInvite.role,
|
||||
});
|
||||
closeInviteModal();
|
||||
createToast({
|
||||
body: "Member invited. They will receive an email with details on how to create their account.",
|
||||
});
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: error.message || "Unknown error.",
|
||||
});
|
||||
} finally {
|
||||
setIsSendingInvite(false);
|
||||
}
|
||||
};
|
||||
|
||||
const closeInviteModal = () => {
|
||||
setIsOpen(false);
|
||||
setToInvite({ email: "", role: ["0"] });
|
||||
setErrors({});
|
||||
};
|
||||
const closeInviteModal = () => {
|
||||
setIsOpen(false);
|
||||
setToInvite({ email: "", role: ["0"] });
|
||||
setErrors({});
|
||||
};
|
||||
|
||||
return (
|
||||
<TabPanel
|
||||
className="team-panel table-container"
|
||||
value="team"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
"& .MuiTable-root .MuiTableBody-root .MuiTableCell-root, & .MuiTable-root p + p":
|
||||
{
|
||||
color: theme.palette.text.accent,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* FEATURE STILL TO BE IMPLEMENTED */}
|
||||
{/* <Stack component="form">
|
||||
return (
|
||||
<TabPanel
|
||||
className="team-panel table-container"
|
||||
value="team"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
"& .MuiTable-root .MuiTableBody-root .MuiTableCell-root, & .MuiTable-root p + p":
|
||||
{
|
||||
color: theme.palette.text.accent,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* FEATURE STILL TO BE IMPLEMENTED */}
|
||||
{/* <Stack component="form">
|
||||
<Box sx={{ alignSelf: "flex-start" }}>
|
||||
<Typography component="h1">Organization name</Typography>
|
||||
</Box>
|
||||
@@ -282,161 +270,163 @@ const TeamPanel = () => {
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Divider aria-hidden="true" sx={{ marginY: theme.spacing(4) }} /> */}
|
||||
<Stack
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={theme.spacing(12)}
|
||||
>
|
||||
<Typography component="h1">Team members</Typography>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="flex-end"
|
||||
gap={theme.spacing(6)}
|
||||
sx={{ fontSize: 14 }}
|
||||
>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(filter === "all").toString()}
|
||||
onClick={() => setFilter("all")}
|
||||
>
|
||||
All
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(filter === "admin").toString()}
|
||||
onClick={() => setFilter("admin")}
|
||||
>
|
||||
Administrator
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(filter === "user").toString()}
|
||||
onClick={() => setFilter("user")}
|
||||
>
|
||||
Member
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
<LoadingButton
|
||||
loading={isSendingInvite}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
Invite a team member
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
<BasicTable
|
||||
data={tableData}
|
||||
paginated={false}
|
||||
reversed={true}
|
||||
table={"team"}
|
||||
/>
|
||||
</Stack>
|
||||
<Modal
|
||||
aria-labelledby="modal-invite-member"
|
||||
aria-describedby="invite-member-to-team"
|
||||
open={isOpen}
|
||||
onClose={closeInviteModal}
|
||||
>
|
||||
<Stack
|
||||
gap={theme.spacing(5)}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 450,
|
||||
bgcolor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxShadow: theme.shape.boxShadow,
|
||||
p: theme.spacing(15),
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography
|
||||
id="modal-invite-member"
|
||||
component="h1"
|
||||
fontWeight={600}
|
||||
fontColor={theme.palette.text.secondary}
|
||||
>
|
||||
Invite new team member
|
||||
</Typography>
|
||||
<Typography
|
||||
id="invite-member-to-team"
|
||||
component="p"
|
||||
fontSize={13}
|
||||
color={theme.palette.text.accent}
|
||||
sx={{ mt: theme.spacing(1), mb: theme.spacing(4) }}
|
||||
>
|
||||
When you add a new team member, they will get access to all
|
||||
monitors.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Field
|
||||
type="email"
|
||||
id="input-team-member"
|
||||
placeholder="Email"
|
||||
value={toInvite.email}
|
||||
onChange={handleChange}
|
||||
error={errors.email}
|
||||
/>
|
||||
<Select
|
||||
id="team-member-role"
|
||||
placeholder="Select role"
|
||||
isHidden={true}
|
||||
value={toInvite.role[0]}
|
||||
onChange={(event) =>
|
||||
setToInvite((prev) => ({
|
||||
...prev,
|
||||
role: [event.target.value],
|
||||
}))
|
||||
}
|
||||
items={[
|
||||
{ _id: "admin", name: "Admin" },
|
||||
{ _id: "user", name: "User" },
|
||||
]}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
mt={theme.spacing(8)}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<LoadingButton
|
||||
loading={isSendingInvite}
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={closeInviteModal}
|
||||
>
|
||||
Cancel
|
||||
</LoadingButton>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleInviteMember}
|
||||
loading={isSendingInvite}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Send invite
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</TabPanel>
|
||||
);
|
||||
<Stack
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={theme.spacing(12)}
|
||||
>
|
||||
<Typography component="h1">Team members</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="flex-end"
|
||||
gap={theme.spacing(6)}
|
||||
sx={{ fontSize: 14 }}
|
||||
>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(filter === "all").toString()}
|
||||
onClick={() => setFilter("all")}
|
||||
>
|
||||
All
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(filter === "admin").toString()}
|
||||
onClick={() => setFilter("admin")}
|
||||
>
|
||||
Administrator
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(filter === "user").toString()}
|
||||
onClick={() => setFilter("user")}
|
||||
>
|
||||
Member
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
<LoadingButton
|
||||
loading={isSendingInvite}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
Invite a team member
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
<BasicTable
|
||||
data={tableData}
|
||||
paginated={false}
|
||||
reversed={true}
|
||||
table={"team"}
|
||||
/>
|
||||
</Stack>
|
||||
<Modal
|
||||
aria-labelledby="modal-invite-member"
|
||||
aria-describedby="invite-member-to-team"
|
||||
open={isOpen}
|
||||
onClose={closeInviteModal}
|
||||
>
|
||||
<Stack
|
||||
gap={theme.spacing(5)}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 450,
|
||||
bgcolor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxShadow: theme.shape.boxShadow,
|
||||
p: theme.spacing(15),
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography
|
||||
id="modal-invite-member"
|
||||
component="h1"
|
||||
fontWeight={600}
|
||||
fontColor={theme.palette.text.secondary}
|
||||
>
|
||||
Invite new team member
|
||||
</Typography>
|
||||
<Typography
|
||||
id="invite-member-to-team"
|
||||
component="p"
|
||||
fontSize={13}
|
||||
color={theme.palette.text.accent}
|
||||
sx={{ mt: theme.spacing(1), mb: theme.spacing(4) }}
|
||||
>
|
||||
When you add a new team member, they will get access to all monitors.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Field
|
||||
type="email"
|
||||
id="input-team-member"
|
||||
placeholder="Email"
|
||||
value={toInvite.email}
|
||||
onChange={handleChange}
|
||||
error={errors.email}
|
||||
/>
|
||||
<Select
|
||||
id="team-member-role"
|
||||
placeholder="Select role"
|
||||
isHidden={true}
|
||||
value={toInvite.role[0]}
|
||||
onChange={(event) =>
|
||||
setToInvite((prev) => ({
|
||||
...prev,
|
||||
role: [event.target.value],
|
||||
}))
|
||||
}
|
||||
items={[
|
||||
{ _id: "admin", name: "Admin" },
|
||||
{ _id: "user", name: "User" },
|
||||
]}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
mt={theme.spacing(8)}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<LoadingButton
|
||||
loading={isSendingInvite}
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={closeInviteModal}
|
||||
>
|
||||
Cancel
|
||||
</LoadingButton>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleInviteMember}
|
||||
loading={isSendingInvite}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Send invite
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</TabPanel>
|
||||
);
|
||||
};
|
||||
|
||||
TeamPanel.propTypes = {
|
||||
// No props are being passed to this component, hence no specific PropTypes are defined.
|
||||
// No props are being passed to this component, hence no specific PropTypes are defined.
|
||||
};
|
||||
|
||||
export default TeamPanel;
|
||||
|
||||
@@ -4,270 +4,254 @@ import { jwtDecode } from "jwt-decode";
|
||||
import axios from "axios";
|
||||
|
||||
const initialState = {
|
||||
isLoading: false,
|
||||
authToken: "",
|
||||
user: "",
|
||||
success: null,
|
||||
msg: null,
|
||||
isLoading: false,
|
||||
authToken: "",
|
||||
user: "",
|
||||
success: null,
|
||||
msg: null,
|
||||
};
|
||||
|
||||
export const register = createAsyncThunk(
|
||||
"auth/register",
|
||||
async (form, thunkApi) => {
|
||||
try {
|
||||
const res = await networkService.registerUser(form);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (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 login = createAsyncThunk("auth/login", async (form, thunkApi) => {
|
||||
try {
|
||||
const res = await networkService.loginUser(form);
|
||||
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 register = createAsyncThunk("auth/register", async (form, thunkApi) => {
|
||||
try {
|
||||
const res = await networkService.registerUser(form);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (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 update = createAsyncThunk(
|
||||
"auth/update",
|
||||
async (data, thunkApi) => {
|
||||
const { authToken: token, localData: form } = data;
|
||||
const user = jwtDecode(token);
|
||||
try {
|
||||
const fd = new FormData();
|
||||
form.firstName && fd.append("firstName", form.firstName);
|
||||
form.lastName && fd.append("lastName", form.lastName);
|
||||
form.password && fd.append("password", form.password);
|
||||
form.newPassword && fd.append("newPassword", form.newPassword);
|
||||
if (form.file && form.file !== "") {
|
||||
const imageResult = await axios.get(form.file, {
|
||||
responseType: "blob",
|
||||
});
|
||||
fd.append("profileImage", imageResult.data);
|
||||
}
|
||||
form.deleteProfileImage &&
|
||||
fd.append("deleteProfileImage", form.deleteProfileImage);
|
||||
export const login = createAsyncThunk("auth/login", async (form, thunkApi) => {
|
||||
try {
|
||||
const res = await networkService.loginUser(form);
|
||||
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 res = await networkService.updateUser({
|
||||
authToken: token,
|
||||
userId: user._id,
|
||||
form: fd,
|
||||
});
|
||||
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 update = createAsyncThunk("auth/update", async (data, thunkApi) => {
|
||||
const { authToken: token, localData: form } = data;
|
||||
const user = jwtDecode(token);
|
||||
try {
|
||||
const fd = new FormData();
|
||||
form.firstName && fd.append("firstName", form.firstName);
|
||||
form.lastName && fd.append("lastName", form.lastName);
|
||||
form.password && fd.append("password", form.password);
|
||||
form.newPassword && fd.append("newPassword", form.newPassword);
|
||||
if (form.file && form.file !== "") {
|
||||
const imageResult = await axios.get(form.file, {
|
||||
responseType: "blob",
|
||||
});
|
||||
fd.append("profileImage", imageResult.data);
|
||||
}
|
||||
form.deleteProfileImage && fd.append("deleteProfileImage", form.deleteProfileImage);
|
||||
|
||||
export const deleteUser = createAsyncThunk(
|
||||
"auth/delete",
|
||||
async (data, thunkApi) => {
|
||||
const user = jwtDecode(data);
|
||||
const res = await networkService.updateUser({
|
||||
authToken: token,
|
||||
userId: user._id,
|
||||
form: fd,
|
||||
});
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await networkService.deleteUser({
|
||||
authToken: data,
|
||||
userId: user._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 deleteUser = createAsyncThunk("auth/delete", async (data, thunkApi) => {
|
||||
const user = jwtDecode(data);
|
||||
|
||||
try {
|
||||
const res = await networkService.deleteUser({
|
||||
authToken: data,
|
||||
userId: user._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 forgotPassword = createAsyncThunk(
|
||||
"auth/forgotPassword",
|
||||
async (form, thunkApi) => {
|
||||
try {
|
||||
const res = await networkService.forgotPassword(form);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
"auth/forgotPassword",
|
||||
async (form, thunkApi) => {
|
||||
try {
|
||||
const res = await networkService.forgotPassword(form);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (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 setNewPassword = createAsyncThunk(
|
||||
"auth/setNewPassword",
|
||||
async (data, thunkApi) => {
|
||||
const { token, form } = data;
|
||||
try {
|
||||
await networkService.validateRecoveryToken({ recoveryToken: token });
|
||||
const res = await networkService.setNewPassword({
|
||||
recoveryToken: token,
|
||||
form: form,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
"auth/setNewPassword",
|
||||
async (data, thunkApi) => {
|
||||
const { token, form } = data;
|
||||
try {
|
||||
await networkService.validateRecoveryToken({ recoveryToken: token });
|
||||
const res = await networkService.setNewPassword({
|
||||
recoveryToken: token,
|
||||
form: form,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (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 handleAuthFulfilled = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
state.authToken = action.payload.data.token;
|
||||
state.user = action.payload.data.user;
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
state.authToken = action.payload.data.token;
|
||||
state.user = action.payload.data.user;
|
||||
};
|
||||
const handleAuthRejected = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to login or register";
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload ? action.payload.msg : "Failed to login or register";
|
||||
};
|
||||
const handleUpdateFulfilled = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
state.user = action.payload.data;
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
state.user = action.payload.data;
|
||||
};
|
||||
const handleUpdateRejected = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to update profile data.";
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload ? action.payload.msg : "Failed to update profile data.";
|
||||
};
|
||||
const handleDeleteFulfilled = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
};
|
||||
const handleDeleteRejected = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload ? action.payload.msg : "Failed to delete account.";
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload ? action.payload.msg : "Failed to delete account.";
|
||||
};
|
||||
const handleForgotFulfilled = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
};
|
||||
const handleForgotRejected = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to send reset instructions.";
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload ? action.payload.msg : "Failed to send reset instructions.";
|
||||
};
|
||||
const handleNewPasswordRejected = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload ? action.payload.msg : "Failed to reset password.";
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload ? action.payload.msg : "Failed to reset password.";
|
||||
};
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: "auth",
|
||||
initialState,
|
||||
reducers: {
|
||||
clearAuthState: (state) => {
|
||||
state.authToken = "";
|
||||
state.user = "";
|
||||
state.isLoading = false;
|
||||
state.success = true;
|
||||
state.msg = "Logged out successfully";
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// Register thunk
|
||||
builder
|
||||
.addCase(register.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(register.fulfilled, handleAuthFulfilled)
|
||||
.addCase(register.rejected, handleAuthRejected);
|
||||
name: "auth",
|
||||
initialState,
|
||||
reducers: {
|
||||
clearAuthState: (state) => {
|
||||
state.authToken = "";
|
||||
state.user = "";
|
||||
state.isLoading = false;
|
||||
state.success = true;
|
||||
state.msg = "Logged out successfully";
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// Register thunk
|
||||
builder
|
||||
.addCase(register.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(register.fulfilled, handleAuthFulfilled)
|
||||
.addCase(register.rejected, handleAuthRejected);
|
||||
|
||||
// Login thunk
|
||||
builder
|
||||
.addCase(login.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(login.fulfilled, handleAuthFulfilled)
|
||||
.addCase(login.rejected, handleAuthRejected);
|
||||
// Login thunk
|
||||
builder
|
||||
.addCase(login.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(login.fulfilled, handleAuthFulfilled)
|
||||
.addCase(login.rejected, handleAuthRejected);
|
||||
|
||||
// Update thunk
|
||||
builder
|
||||
.addCase(update.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(update.fulfilled, handleUpdateFulfilled)
|
||||
.addCase(update.rejected, handleUpdateRejected);
|
||||
// Update thunk
|
||||
builder
|
||||
.addCase(update.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(update.fulfilled, handleUpdateFulfilled)
|
||||
.addCase(update.rejected, handleUpdateRejected);
|
||||
|
||||
// Delete thunk
|
||||
builder
|
||||
.addCase(deleteUser.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deleteUser.fulfilled, handleDeleteFulfilled)
|
||||
.addCase(deleteUser.rejected, handleDeleteRejected);
|
||||
// Delete thunk
|
||||
builder
|
||||
.addCase(deleteUser.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deleteUser.fulfilled, handleDeleteFulfilled)
|
||||
.addCase(deleteUser.rejected, handleDeleteRejected);
|
||||
|
||||
// Forgot password thunk
|
||||
builder
|
||||
.addCase(forgotPassword.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(forgotPassword.fulfilled, handleForgotFulfilled)
|
||||
.addCase(forgotPassword.rejected, handleForgotRejected);
|
||||
// Forgot password thunk
|
||||
builder
|
||||
.addCase(forgotPassword.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(forgotPassword.fulfilled, handleForgotFulfilled)
|
||||
.addCase(forgotPassword.rejected, handleForgotRejected);
|
||||
|
||||
// Set new password thunk
|
||||
builder
|
||||
.addCase(setNewPassword.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(setNewPassword.fulfilled, handleAuthFulfilled)
|
||||
.addCase(setNewPassword.rejected, handleNewPasswordRejected);
|
||||
},
|
||||
// Set new password thunk
|
||||
builder
|
||||
.addCase(setNewPassword.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(setNewPassword.fulfilled, handleAuthFulfilled)
|
||||
.addCase(setNewPassword.rejected, handleNewPasswordRejected);
|
||||
},
|
||||
});
|
||||
|
||||
export default authSlice.reducer;
|
||||
|
||||
@@ -2,283 +2,283 @@ import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { networkService } from "../../main";
|
||||
const initialState = {
|
||||
isLoading: false,
|
||||
monitorsSummary: [],
|
||||
success: null,
|
||||
msg: null,
|
||||
isLoading: false,
|
||||
monitorsSummary: [],
|
||||
success: null,
|
||||
msg: null,
|
||||
};
|
||||
|
||||
export const createPageSpeed = createAsyncThunk(
|
||||
"pageSpeedMonitors/createPageSpeed",
|
||||
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);
|
||||
}
|
||||
}
|
||||
"pageSpeedMonitors/createPageSpeed",
|
||||
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 getPagespeedMonitorById = createAsyncThunk(
|
||||
"monitors/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);
|
||||
}
|
||||
}
|
||||
"monitors/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 getPageSpeedByTeamId = createAsyncThunk(
|
||||
"pageSpeedMonitors/getPageSpeedByTeamId",
|
||||
async (token, thunkApi) => {
|
||||
const user = jwtDecode(token);
|
||||
try {
|
||||
const res = await networkService.getMonitorsAndSummaryByTeamId({
|
||||
authToken: token,
|
||||
teamId: user.teamId,
|
||||
types: ["pagespeed"],
|
||||
});
|
||||
"pageSpeedMonitors/getPageSpeedByTeamId",
|
||||
async (token, thunkApi) => {
|
||||
const user = jwtDecode(token);
|
||||
try {
|
||||
const res = await networkService.getMonitorsAndSummaryByTeamId({
|
||||
authToken: token,
|
||||
teamId: user.teamId,
|
||||
types: ["pagespeed"],
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
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 updatePageSpeed = createAsyncThunk(
|
||||
"pageSpeedMonitors/updatePageSpeed",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitor } = data;
|
||||
const updatedFields = {
|
||||
name: monitor.name,
|
||||
description: monitor.description,
|
||||
interval: monitor.interval,
|
||||
// notifications: monitor.notifications,
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
||||
"pageSpeedMonitors/updatePageSpeed",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitor } = data;
|
||||
const updatedFields = {
|
||||
name: monitor.name,
|
||||
description: monitor.description,
|
||||
interval: monitor.interval,
|
||||
// notifications: monitor.notifications,
|
||||
};
|
||||
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 deletePageSpeed = createAsyncThunk(
|
||||
"pageSpeedMonitors/deletePageSpeed",
|
||||
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);
|
||||
}
|
||||
}
|
||||
"pageSpeedMonitors/deletePageSpeed",
|
||||
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 pausePageSpeed = createAsyncThunk(
|
||||
"pageSpeedMonitors/pausePageSpeed",
|
||||
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);
|
||||
}
|
||||
}
|
||||
"pageSpeedMonitors/pausePageSpeed",
|
||||
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);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const pageSpeedMonitorSlice = createSlice({
|
||||
name: "pageSpeedMonitor",
|
||||
initialState,
|
||||
reducers: {
|
||||
clearMonitorState: (state) => {
|
||||
state.isLoading = false;
|
||||
state.monitorsSummary = [];
|
||||
state.success = null;
|
||||
state.msg = null;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// *****************************************************
|
||||
// Monitors by teamId
|
||||
// *****************************************************
|
||||
name: "pageSpeedMonitor",
|
||||
initialState,
|
||||
reducers: {
|
||||
clearMonitorState: (state) => {
|
||||
state.isLoading = false;
|
||||
state.monitorsSummary = [];
|
||||
state.success = null;
|
||||
state.msg = null;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// *****************************************************
|
||||
// Monitors by teamId
|
||||
// *****************************************************
|
||||
|
||||
.addCase(getPageSpeedByTeamId.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getPageSpeedByTeamId.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.msg;
|
||||
state.monitorsSummary = action.payload.data;
|
||||
})
|
||||
.addCase(getPageSpeedByTeamId.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Getting page speed monitors failed";
|
||||
})
|
||||
.addCase(getPageSpeedByTeamId.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getPageSpeedByTeamId.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.msg;
|
||||
state.monitorsSummary = action.payload.data;
|
||||
})
|
||||
.addCase(getPageSpeedByTeamId.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Getting page speed monitors failed";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
.addCase(getPagespeedMonitorById.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getPagespeedMonitorById.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(getPagespeedMonitorById.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to get pagespeed monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
.addCase(getPagespeedMonitorById.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getPagespeedMonitorById.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(getPagespeedMonitorById.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to get pagespeed monitor";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// Create Monitor
|
||||
// *****************************************************
|
||||
.addCase(createPageSpeed.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(createPageSpeed.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(createPageSpeed.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to create page speed monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Create Monitor
|
||||
// *****************************************************
|
||||
.addCase(createPageSpeed.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(createPageSpeed.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(createPageSpeed.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to create page speed monitor";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// Update Monitor
|
||||
// *****************************************************
|
||||
.addCase(updatePageSpeed.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(updatePageSpeed.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(updatePageSpeed.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to update page speed monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Update Monitor
|
||||
// *****************************************************
|
||||
.addCase(updatePageSpeed.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(updatePageSpeed.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(updatePageSpeed.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to update page speed monitor";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// Delete Monitor
|
||||
// *****************************************************
|
||||
.addCase(deletePageSpeed.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deletePageSpeed.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(deletePageSpeed.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to delete page speed monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Pause Monitor
|
||||
// *****************************************************
|
||||
.addCase(pausePageSpeed.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(pausePageSpeed.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(pausePageSpeed.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to pause page speed monitor";
|
||||
});
|
||||
},
|
||||
// *****************************************************
|
||||
// Delete Monitor
|
||||
// *****************************************************
|
||||
.addCase(deletePageSpeed.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deletePageSpeed.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(deletePageSpeed.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to delete page speed monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Pause Monitor
|
||||
// *****************************************************
|
||||
.addCase(pausePageSpeed.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(pausePageSpeed.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(pausePageSpeed.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to pause page speed monitor";
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { setMonitors, clearMonitorState } = pageSpeedMonitorSlice.actions;
|
||||
|
||||
@@ -2,116 +2,114 @@ import { networkService } from "../../main";
|
||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
const initialState = {
|
||||
isLoading: false,
|
||||
apiBaseUrl: "",
|
||||
logLevel: "debug",
|
||||
isLoading: false,
|
||||
apiBaseUrl: "",
|
||||
logLevel: "debug",
|
||||
};
|
||||
|
||||
export const getAppSettings = createAsyncThunk(
|
||||
"settings/getSettings",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const res = await networkService.getAppSettings({
|
||||
authToken: data.authToken,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
"settings/getSettings",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const res = await networkService.getAppSettings({
|
||||
authToken: data.authToken,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (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 updateAppSettings = createAsyncThunk(
|
||||
"settings/updateSettings",
|
||||
async ({ settings, authToken }, thunkApi) => {
|
||||
networkService.setBaseUrl(settings.apiBaseUrl);
|
||||
try {
|
||||
const parsedSettings = {
|
||||
apiBaseUrl: settings.apiBaseUrl,
|
||||
logLevel: settings.logLevel,
|
||||
clientHost: settings.clientHost,
|
||||
jwtSecret: settings.jwtSecret,
|
||||
dbType: settings.dbType,
|
||||
dbConnectionString: settings.dbConnectionString,
|
||||
redisHost: settings.redisHost,
|
||||
redisPort: settings.redisPort,
|
||||
jwtTTL: settings.jwtTTL,
|
||||
pagespeedApiKey: settings.pagespeedApiKey,
|
||||
systemEmailHost: settings.systemEmailHost,
|
||||
systemEmailPort: settings.systemEmailPort,
|
||||
systemEmailAddress: settings.systemEmailAddress,
|
||||
systemEmailPassword: settings.systemEmailPassword,
|
||||
};
|
||||
const res = await networkService.updateAppSettings({
|
||||
settings: parsedSettings,
|
||||
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);
|
||||
}
|
||||
}
|
||||
"settings/updateSettings",
|
||||
async ({ settings, authToken }, thunkApi) => {
|
||||
networkService.setBaseUrl(settings.apiBaseUrl);
|
||||
try {
|
||||
const parsedSettings = {
|
||||
apiBaseUrl: settings.apiBaseUrl,
|
||||
logLevel: settings.logLevel,
|
||||
clientHost: settings.clientHost,
|
||||
jwtSecret: settings.jwtSecret,
|
||||
dbType: settings.dbType,
|
||||
dbConnectionString: settings.dbConnectionString,
|
||||
redisHost: settings.redisHost,
|
||||
redisPort: settings.redisPort,
|
||||
jwtTTL: settings.jwtTTL,
|
||||
pagespeedApiKey: settings.pagespeedApiKey,
|
||||
systemEmailHost: settings.systemEmailHost,
|
||||
systemEmailPort: settings.systemEmailPort,
|
||||
systemEmailAddress: settings.systemEmailAddress,
|
||||
systemEmailPassword: settings.systemEmailPassword,
|
||||
};
|
||||
const res = await networkService.updateAppSettings({
|
||||
settings: parsedSettings,
|
||||
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 handleGetSettingsFulfilled = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
state.apiBaseUrl = action.payload.data.apiBaseUrl;
|
||||
state.logLevel = action.payload.data.logLevel;
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
state.apiBaseUrl = action.payload.data.apiBaseUrl;
|
||||
state.logLevel = action.payload.data.logLevel;
|
||||
};
|
||||
const handleGetSettingsRejected = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload ? action.payload.msg : "Failed to get settings.";
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload ? action.payload.msg : "Failed to get settings.";
|
||||
};
|
||||
const handleUpdateSettingsFulfilled = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
state.apiBaseUrl = action.payload.data.apiBaseUrl;
|
||||
state.logLevel = action.payload.data.logLevel;
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
state.apiBaseUrl = action.payload.data.apiBaseUrl;
|
||||
state.logLevel = action.payload.data.logLevel;
|
||||
};
|
||||
const handleUpdateSettingsRejected = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to update settings.";
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload ? action.payload.msg : "Failed to update settings.";
|
||||
};
|
||||
|
||||
const settingsSlice = createSlice({
|
||||
name: "settings",
|
||||
initialState,
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(getAppSettings.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getAppSettings.fulfilled, handleGetSettingsFulfilled)
|
||||
.addCase(getAppSettings.rejected, handleGetSettingsRejected);
|
||||
name: "settings",
|
||||
initialState,
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(getAppSettings.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getAppSettings.fulfilled, handleGetSettingsFulfilled)
|
||||
.addCase(getAppSettings.rejected, handleGetSettingsRejected);
|
||||
|
||||
builder
|
||||
.addCase(updateAppSettings.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(updateAppSettings.fulfilled, handleUpdateSettingsFulfilled)
|
||||
.addCase(updateAppSettings.rejected, handleUpdateSettingsRejected);
|
||||
},
|
||||
builder
|
||||
.addCase(updateAppSettings.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(updateAppSettings.fulfilled, handleUpdateSettingsFulfilled)
|
||||
.addCase(updateAppSettings.rejected, handleUpdateSettingsRejected);
|
||||
},
|
||||
});
|
||||
|
||||
export default settingsSlice.reducer;
|
||||
|
||||
@@ -3,54 +3,49 @@ import { createSlice } from "@reduxjs/toolkit";
|
||||
// Initial state for UI settings.
|
||||
// Add more settings as needed (e.g., theme preferences, user settings)
|
||||
const initialState = {
|
||||
monitors: {
|
||||
rowsPerPage: 10,
|
||||
},
|
||||
team: {
|
||||
rowsPerPage: 5,
|
||||
},
|
||||
maintenance: {
|
||||
rowsPerPage: 5,
|
||||
},
|
||||
sidebar: {
|
||||
collapsed: false,
|
||||
},
|
||||
mode: "light",
|
||||
greeting: { index: 0, lastUpdate: null },
|
||||
timezone: "America/Toronto",
|
||||
monitors: {
|
||||
rowsPerPage: 10,
|
||||
},
|
||||
team: {
|
||||
rowsPerPage: 5,
|
||||
},
|
||||
maintenance: {
|
||||
rowsPerPage: 5,
|
||||
},
|
||||
sidebar: {
|
||||
collapsed: false,
|
||||
},
|
||||
mode: "light",
|
||||
greeting: { index: 0, lastUpdate: null },
|
||||
timezone: "America/Toronto",
|
||||
};
|
||||
|
||||
const uiSlice = createSlice({
|
||||
name: "ui",
|
||||
initialState,
|
||||
reducers: {
|
||||
setRowsPerPage: (state, action) => {
|
||||
const { table, value } = action.payload;
|
||||
if (state[table]) {
|
||||
state[table].rowsPerPage = value;
|
||||
}
|
||||
},
|
||||
toggleSidebar: (state) => {
|
||||
state.sidebar.collapsed = !state.sidebar.collapsed;
|
||||
},
|
||||
setMode: (state, action) => {
|
||||
state.mode = action.payload;
|
||||
},
|
||||
setGreeting(state, action) {
|
||||
state.greeting.index = action.payload.index;
|
||||
state.greeting.lastUpdate = action.payload.lastUpdate;
|
||||
},
|
||||
setTimezone(state, action) {
|
||||
state.timezone = action.payload.timezone;
|
||||
},
|
||||
},
|
||||
name: "ui",
|
||||
initialState,
|
||||
reducers: {
|
||||
setRowsPerPage: (state, action) => {
|
||||
const { table, value } = action.payload;
|
||||
if (state[table]) {
|
||||
state[table].rowsPerPage = value;
|
||||
}
|
||||
},
|
||||
toggleSidebar: (state) => {
|
||||
state.sidebar.collapsed = !state.sidebar.collapsed;
|
||||
},
|
||||
setMode: (state, action) => {
|
||||
state.mode = action.payload;
|
||||
},
|
||||
setGreeting(state, action) {
|
||||
state.greeting.index = action.payload.index;
|
||||
state.greeting.lastUpdate = action.payload.lastUpdate;
|
||||
},
|
||||
setTimezone(state, action) {
|
||||
state.timezone = action.payload.timezone;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default uiSlice.reducer;
|
||||
export const {
|
||||
setRowsPerPage,
|
||||
toggleSidebar,
|
||||
setMode,
|
||||
setGreeting,
|
||||
setTimezone,
|
||||
} = uiSlice.actions;
|
||||
export const { setRowsPerPage, toggleSidebar, setMode, setGreeting, setTimezone } =
|
||||
uiSlice.actions;
|
||||
|
||||
@@ -2,447 +2,442 @@ import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { networkService } from "../../main";
|
||||
const initialState = {
|
||||
isLoading: false,
|
||||
monitorsSummary: [],
|
||||
success: null,
|
||||
msg: null,
|
||||
isLoading: false,
|
||||
monitorsSummary: [],
|
||||
success: null,
|
||||
msg: null,
|
||||
};
|
||||
|
||||
export const createUptimeMonitor = createAsyncThunk(
|
||||
"monitors/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);
|
||||
}
|
||||
}
|
||||
"monitors/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 checkEndpointResolution = createAsyncThunk(
|
||||
"monitors/checkEndpoint",
|
||||
async(data, thunkApi) => {
|
||||
try{
|
||||
const { authToken, monitorURL } = data;
|
||||
"monitors/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);
|
||||
}
|
||||
}
|
||||
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 getUptimeMonitorById = createAsyncThunk(
|
||||
"monitors/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);
|
||||
}
|
||||
}
|
||||
"monitors/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 getUptimeMonitorsByTeamId = createAsyncThunk(
|
||||
"monitors/getMonitorsByTeamId",
|
||||
async (token, thunkApi) => {
|
||||
const user = jwtDecode(token);
|
||||
try {
|
||||
const res = await networkService.getMonitorsAndSummaryByTeamId({
|
||||
authToken: token,
|
||||
teamId: user.teamId,
|
||||
types: ["http", "ping"],
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
"monitors/getMonitorsByTeamId",
|
||||
async (token, thunkApi) => {
|
||||
const user = jwtDecode(token);
|
||||
try {
|
||||
const res = await networkService.getMonitorsAndSummaryByTeamId({
|
||||
authToken: token,
|
||||
teamId: user.teamId,
|
||||
types: ["http", "ping"],
|
||||
});
|
||||
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 updateUptimeMonitor = createAsyncThunk(
|
||||
"monitors/updateMonitor",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitor } = data;
|
||||
const updatedFields = {
|
||||
name: monitor.name,
|
||||
description: monitor.description,
|
||||
interval: monitor.interval,
|
||||
notifications: monitor.notifications,
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
||||
"monitors/updateMonitor",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitor } = data;
|
||||
const updatedFields = {
|
||||
name: monitor.name,
|
||||
description: monitor.description,
|
||||
interval: monitor.interval,
|
||||
notifications: monitor.notifications,
|
||||
};
|
||||
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 deleteUptimeMonitor = createAsyncThunk(
|
||||
"monitors/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);
|
||||
}
|
||||
}
|
||||
"monitors/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 pauseUptimeMonitor = createAsyncThunk(
|
||||
"monitors/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);
|
||||
}
|
||||
}
|
||||
"monitors/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 deleteMonitorChecksByTeamId = createAsyncThunk(
|
||||
"monitors/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);
|
||||
}
|
||||
}
|
||||
"monitors/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 addDemoMonitors = createAsyncThunk(
|
||||
"monitors/addDemoMonitors",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken } = data;
|
||||
const res = await networkService.addDemoMonitors({
|
||||
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);
|
||||
}
|
||||
}
|
||||
"monitors/addDemoMonitors",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken } = data;
|
||||
const res = await networkService.addDemoMonitors({
|
||||
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);
|
||||
}
|
||||
}
|
||||
);
|
||||
export const deleteAllMonitors = createAsyncThunk(
|
||||
"monitors/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);
|
||||
}
|
||||
}
|
||||
"monitors/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 uptimeMonitorsSlice = createSlice({
|
||||
name: "uptimeMonitors",
|
||||
initialState,
|
||||
reducers: {
|
||||
clearUptimeMonitorState: (state) => {
|
||||
state.isLoading = false;
|
||||
state.monitorsSummary = [];
|
||||
state.success = null;
|
||||
state.msg = null;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// *****************************************************
|
||||
// Monitors by teamId
|
||||
// *****************************************************
|
||||
name: "uptimeMonitors",
|
||||
initialState,
|
||||
reducers: {
|
||||
clearUptimeMonitorState: (state) => {
|
||||
state.isLoading = false;
|
||||
state.monitorsSummary = [];
|
||||
state.success = null;
|
||||
state.msg = null;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// *****************************************************
|
||||
// Monitors by teamId
|
||||
// *****************************************************
|
||||
|
||||
.addCase(getUptimeMonitorsByTeamId.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getUptimeMonitorsByTeamId.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.msg;
|
||||
state.monitorsSummary = action.payload.data;
|
||||
})
|
||||
.addCase(getUptimeMonitorsByTeamId.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Getting uptime monitors failed";
|
||||
})
|
||||
.addCase(getUptimeMonitorsByTeamId.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getUptimeMonitorsByTeamId.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.msg;
|
||||
state.monitorsSummary = action.payload.data;
|
||||
})
|
||||
.addCase(getUptimeMonitorsByTeamId.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Getting uptime monitors failed";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// Create Monitor
|
||||
// *****************************************************
|
||||
.addCase(createUptimeMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(createUptimeMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(createUptimeMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to create uptime monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Resolve Endpoint
|
||||
// *****************************************************
|
||||
.addCase(checkEndpointResolution.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(checkEndpointResolution.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(checkEndpointResolution.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(getUptimeMonitorById.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getUptimeMonitorById.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(getUptimeMonitorById.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to get uptime monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// update Monitor
|
||||
// *****************************************************
|
||||
.addCase(updateUptimeMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(updateUptimeMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(updateUptimeMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to update uptime monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Create Monitor
|
||||
// *****************************************************
|
||||
.addCase(createUptimeMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(createUptimeMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(createUptimeMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to create uptime monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Resolve Endpoint
|
||||
// *****************************************************
|
||||
.addCase(checkEndpointResolution.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(checkEndpointResolution.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(checkEndpointResolution.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(getUptimeMonitorById.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getUptimeMonitorById.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(getUptimeMonitorById.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload ? action.payload.msg : "Failed to get uptime monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// update Monitor
|
||||
// *****************************************************
|
||||
.addCase(updateUptimeMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(updateUptimeMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(updateUptimeMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to update uptime monitor";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// Delete Monitor
|
||||
// *****************************************************
|
||||
.addCase(deleteUptimeMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deleteUptimeMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(deleteUptimeMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to delete uptime monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Delete Monitor checks by Team ID
|
||||
// *****************************************************
|
||||
.addCase(deleteMonitorChecksByTeamId.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deleteMonitorChecksByTeamId.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(deleteMonitorChecksByTeamId.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to delete monitor checks";
|
||||
})
|
||||
// *****************************************************
|
||||
// Pause Monitor
|
||||
// *****************************************************
|
||||
.addCase(pauseUptimeMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(pauseUptimeMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(pauseUptimeMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to pause uptime monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Add Demo Monitors
|
||||
// *****************************************************
|
||||
.addCase(addDemoMonitors.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(addDemoMonitors.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(addDemoMonitors.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to add demo uptime monitors";
|
||||
})
|
||||
// *****************************************************
|
||||
// Delete all Monitors
|
||||
// *****************************************************
|
||||
.addCase(deleteAllMonitors.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deleteAllMonitors.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(deleteAllMonitors.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to delete all monitors";
|
||||
});
|
||||
},
|
||||
// *****************************************************
|
||||
// Delete Monitor
|
||||
// *****************************************************
|
||||
.addCase(deleteUptimeMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deleteUptimeMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(deleteUptimeMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to delete uptime monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Delete Monitor checks by Team ID
|
||||
// *****************************************************
|
||||
.addCase(deleteMonitorChecksByTeamId.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deleteMonitorChecksByTeamId.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(deleteMonitorChecksByTeamId.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to delete monitor checks";
|
||||
})
|
||||
// *****************************************************
|
||||
// Pause Monitor
|
||||
// *****************************************************
|
||||
.addCase(pauseUptimeMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(pauseUptimeMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(pauseUptimeMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to pause uptime monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Add Demo Monitors
|
||||
// *****************************************************
|
||||
.addCase(addDemoMonitors.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(addDemoMonitors.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(addDemoMonitors.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to add demo uptime monitors";
|
||||
})
|
||||
// *****************************************************
|
||||
// Delete all Monitors
|
||||
// *****************************************************
|
||||
.addCase(deleteAllMonitors.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deleteAllMonitors.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(deleteAllMonitors.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload ? action.payload.msg : "Failed to delete all monitors";
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { setUptimeMonitors, clearUptimeMonitorState } =
|
||||
uptimeMonitorsSlice.actions;
|
||||
export const { setUptimeMonitors, clearUptimeMonitorState } = uptimeMonitorsSlice.actions;
|
||||
|
||||
export default uptimeMonitorsSlice.reducer;
|
||||
|
||||
@@ -5,28 +5,33 @@ import { logger } from "../Utils/Logger";
|
||||
import { networkService } from "../main";
|
||||
|
||||
const withAdminCheck = (WrappedComponent) => {
|
||||
const WithAdminCheck = (props) => {
|
||||
const navigate = useNavigate();
|
||||
const WithAdminCheck = (props) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
networkService
|
||||
.doesSuperAdminExist()
|
||||
.then((response) => {
|
||||
if (response.data.data === true) {
|
||||
navigate("/login");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(error);
|
||||
});
|
||||
}, [navigate]);
|
||||
return <WrappedComponent {...props} isSuperAdmin={true} />;
|
||||
};
|
||||
const wrappedComponentName =
|
||||
WrappedComponent.displayName || WrappedComponent.name || "Component";
|
||||
WithAdminCheck.displayName = `WithAdminCheck(${wrappedComponentName})`;
|
||||
useEffect(() => {
|
||||
networkService
|
||||
.doesSuperAdminExist()
|
||||
.then((response) => {
|
||||
if (response.data.data === true) {
|
||||
navigate("/login");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(error);
|
||||
});
|
||||
}, [navigate]);
|
||||
return (
|
||||
<WrappedComponent
|
||||
{...props}
|
||||
isSuperAdmin={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const wrappedComponentName =
|
||||
WrappedComponent.displayName || WrappedComponent.name || "Component";
|
||||
WithAdminCheck.displayName = `WithAdminCheck(${wrappedComponentName})`;
|
||||
|
||||
return WithAdminCheck;
|
||||
return WithAdminCheck;
|
||||
};
|
||||
|
||||
export default withAdminCheck;
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const withAdminProp = (WrappedComponent) => {
|
||||
const WithAdminProp = (props) => {
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const isAdmin =
|
||||
(user?.role?.includes("admin") ?? false) ||
|
||||
(user?.role?.includes("superadmin") ?? false);
|
||||
const WithAdminProp = (props) => {
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const isAdmin =
|
||||
(user?.role?.includes("admin") ?? false) ||
|
||||
(user?.role?.includes("superadmin") ?? false);
|
||||
|
||||
return <WrappedComponent {...props} isAdmin={isAdmin} />;
|
||||
};
|
||||
return (
|
||||
<WrappedComponent
|
||||
{...props}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedComponentName =
|
||||
WrappedComponent.displayName || WrappedComponent.name || "Component";
|
||||
WithAdminProp.displayName = `WithAdminProp(${wrappedComponentName})`;
|
||||
const wrappedComponentName =
|
||||
WrappedComponent.displayName || WrappedComponent.name || "Component";
|
||||
WithAdminProp.displayName = `WithAdminProp(${wrappedComponentName})`;
|
||||
|
||||
return WithAdminProp;
|
||||
return WithAdminProp;
|
||||
};
|
||||
|
||||
export default withAdminProp;
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
.home-layout {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: var(--env-var-spacing-2);
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: var(--env-var-spacing-2);
|
||||
}
|
||||
|
||||
.home-layout aside {
|
||||
position: sticky;
|
||||
top: var(--env-var-spacing-2);
|
||||
left: 0;
|
||||
position: sticky;
|
||||
top: var(--env-var-spacing-2);
|
||||
left: 0;
|
||||
|
||||
height: calc(100vh - var(--env-var-spacing-2) * 2);
|
||||
max-width: var(--env-var-side-bar-width);
|
||||
height: calc(100vh - var(--env-var-spacing-2) * 2);
|
||||
max-width: var(--env-var-side-bar-width);
|
||||
}
|
||||
|
||||
.home-layout > div {
|
||||
min-height: calc(100vh - var(--env-var-spacing-2) * 2);
|
||||
flex: 1;
|
||||
min-height: calc(100vh - var(--env-var-spacing-2) * 2);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.home-layout > div:has(> [class*="fallback__"]) .background-pattern-svg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 0;
|
||||
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
height: 100%;
|
||||
max-height: 800px;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
height: 100%;
|
||||
max-height: 800px;
|
||||
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
@@ -5,12 +5,16 @@ import { Stack } from "@mui/material";
|
||||
import "./index.css";
|
||||
|
||||
const HomeLayout = () => {
|
||||
return (
|
||||
<Stack className="home-layout" flexDirection="row" gap={14}>
|
||||
<Sidebar />
|
||||
<Outlet />
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
className="home-layout"
|
||||
flexDirection="row"
|
||||
gap={14}
|
||||
>
|
||||
<Sidebar />
|
||||
<Outlet />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomeLayout;
|
||||
|
||||
@@ -4,35 +4,35 @@
|
||||
.account button,
|
||||
.account td,
|
||||
.account .MuiSelect-select {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.account h1.MuiTypography-root {
|
||||
font-weight: 600;
|
||||
font-weight: 600;
|
||||
}
|
||||
.account .MuiTabPanel-root {
|
||||
padding: 0;
|
||||
margin-top: 50px;
|
||||
padding: 0;
|
||||
margin-top: 50px;
|
||||
}
|
||||
.account button:not(.MuiIconButton-root) {
|
||||
height: 34px;
|
||||
height: 34px;
|
||||
}
|
||||
.account .field {
|
||||
flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
#modal-delete-account,
|
||||
#modal-edit-org-name,
|
||||
#modal-invite-member {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
font-size: var(--env-var-font-size-large);
|
||||
}
|
||||
|
||||
.account .MuiStack-root:has(span.MuiTypography-root.input-error) {
|
||||
position: relative;
|
||||
position: relative;
|
||||
}
|
||||
.account:not(:has(#modal-invite-member)) span.MuiTypography-root.input-error {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
.account .MuiTableBody-root .MuiTableCell-root {
|
||||
padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2);
|
||||
padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2);
|
||||
}
|
||||
|
||||
@@ -16,78 +16,81 @@ import "./index.css";
|
||||
*/
|
||||
|
||||
const Account = ({ open = "profile" }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const tab = open;
|
||||
const handleTabChange = (event, newTab) => {
|
||||
navigate(`/account/${newTab}`);
|
||||
};
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const tab = open;
|
||||
const handleTabChange = (event, newTab) => {
|
||||
navigate(`/account/${newTab}`);
|
||||
};
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
|
||||
const requiredRoles = ["superadmin", "admin"];
|
||||
let tabList = ["Profile", "Password", "Team"];
|
||||
const hideTeams = !requiredRoles.some((role) => user.role.includes(role));
|
||||
if (hideTeams) {
|
||||
tabList = ["Profile", "Password"];
|
||||
}
|
||||
const requiredRoles = ["superadmin", "admin"];
|
||||
let tabList = ["Profile", "Password", "Team"];
|
||||
const hideTeams = !requiredRoles.some((role) => user.role.includes(role));
|
||||
if (hideTeams) {
|
||||
tabList = ["Profile", "Password"];
|
||||
}
|
||||
|
||||
// Remove password for demo
|
||||
if (user.role.includes("demo")) {
|
||||
tabList = ["Profile"];
|
||||
}
|
||||
// Remove password for demo
|
||||
if (user.role.includes("demo")) {
|
||||
tabList = ["Profile"];
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="account"
|
||||
px={theme.spacing(20)}
|
||||
py={theme.spacing(12)}
|
||||
border={1}
|
||||
borderColor={theme.palette.border.light}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
backgroundColor={theme.palette.background.main}
|
||||
>
|
||||
<TabContext value={tab}>
|
||||
<Box
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
"& .MuiTabs-root": { height: "fit-content", minHeight: "0" },
|
||||
}}
|
||||
>
|
||||
<TabList onChange={handleTabChange} aria-label="account tabs">
|
||||
{tabList.map((label, index) => (
|
||||
<Tab
|
||||
label={label}
|
||||
key={index}
|
||||
value={label.toLowerCase()}
|
||||
sx={{
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.tertiary,
|
||||
textTransform: "none",
|
||||
minWidth: "fit-content",
|
||||
minHeight: 0,
|
||||
paddingLeft: 0,
|
||||
paddingY: theme.spacing(4),
|
||||
fontWeight: 400,
|
||||
marginRight: theme.spacing(8),
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</TabList>
|
||||
</Box>
|
||||
<ProfilePanel />
|
||||
{user.role.includes("superadmin") && <PasswordPanel />}
|
||||
{!hideTeams && <TeamPanel />}
|
||||
</TabContext>
|
||||
</Box>
|
||||
);
|
||||
return (
|
||||
<Box
|
||||
className="account"
|
||||
px={theme.spacing(20)}
|
||||
py={theme.spacing(12)}
|
||||
border={1}
|
||||
borderColor={theme.palette.border.light}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
backgroundColor={theme.palette.background.main}
|
||||
>
|
||||
<TabContext value={tab}>
|
||||
<Box
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
"& .MuiTabs-root": { height: "fit-content", minHeight: "0" },
|
||||
}}
|
||||
>
|
||||
<TabList
|
||||
onChange={handleTabChange}
|
||||
aria-label="account tabs"
|
||||
>
|
||||
{tabList.map((label, index) => (
|
||||
<Tab
|
||||
label={label}
|
||||
key={index}
|
||||
value={label.toLowerCase()}
|
||||
sx={{
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.tertiary,
|
||||
textTransform: "none",
|
||||
minWidth: "fit-content",
|
||||
minHeight: 0,
|
||||
paddingLeft: 0,
|
||||
paddingY: theme.spacing(4),
|
||||
fontWeight: 400,
|
||||
marginRight: theme.spacing(8),
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</TabList>
|
||||
</Box>
|
||||
<ProfilePanel />
|
||||
{user.role.includes("superadmin") && <PasswordPanel />}
|
||||
{!hideTeams && <TeamPanel />}
|
||||
</TabContext>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Account.propTypes = {
|
||||
open: PropTypes.oneOf(["profile", "password", "team"]),
|
||||
open: PropTypes.oneOf(["profile", "password", "team"]),
|
||||
};
|
||||
|
||||
export default Account;
|
||||
|
||||
@@ -9,257 +9,255 @@ import PropTypes from "prop-types";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { ConfigBox } from "../Settings/styled";
|
||||
import { useNavigate } from "react-router";
|
||||
import {
|
||||
getAppSettings,
|
||||
updateAppSettings,
|
||||
} from "../../Features/Settings/settingsSlice";
|
||||
import { getAppSettings, updateAppSettings } from "../../Features/Settings/settingsSlice";
|
||||
import { useState, useEffect } from "react";
|
||||
import Select from "../../Components/Inputs/Select";
|
||||
|
||||
const AdvancedSettings = ({ isAdmin }) => {
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAdmin) {
|
||||
navigate("/");
|
||||
}
|
||||
}, [navigate, isAdmin]);
|
||||
useEffect(() => {
|
||||
if (!isAdmin) {
|
||||
navigate("/");
|
||||
}
|
||||
}, [navigate, isAdmin]);
|
||||
|
||||
const theme = useTheme();
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const dispatch = useDispatch();
|
||||
const settings = useSelector((state) => state.settings);
|
||||
const [localSettings, setLocalSettings] = useState({
|
||||
apiBaseUrl: "",
|
||||
logLevel: "debug",
|
||||
systemEmailHost: "",
|
||||
systemEmailPort: "",
|
||||
systemEmailAddress: "",
|
||||
systemEmailPassword: "",
|
||||
jwtTTL: "",
|
||||
dbType: "",
|
||||
redisHost: "",
|
||||
redisPort: "",
|
||||
pagespeedApiKey: "",
|
||||
});
|
||||
const theme = useTheme();
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const dispatch = useDispatch();
|
||||
const settings = useSelector((state) => state.settings);
|
||||
const [localSettings, setLocalSettings] = useState({
|
||||
apiBaseUrl: "",
|
||||
logLevel: "debug",
|
||||
systemEmailHost: "",
|
||||
systemEmailPort: "",
|
||||
systemEmailAddress: "",
|
||||
systemEmailPassword: "",
|
||||
jwtTTL: "",
|
||||
dbType: "",
|
||||
redisHost: "",
|
||||
redisPort: "",
|
||||
pagespeedApiKey: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const getSettings = async () => {
|
||||
const action = await dispatch(getAppSettings({ authToken }));
|
||||
if (action.payload.success) {
|
||||
setLocalSettings(action.payload.data);
|
||||
} else {
|
||||
createToast({ body: "Failed to get settings" });
|
||||
}
|
||||
};
|
||||
getSettings();
|
||||
}, [authToken, dispatch]);
|
||||
useEffect(() => {
|
||||
const getSettings = async () => {
|
||||
const action = await dispatch(getAppSettings({ authToken }));
|
||||
if (action.payload.success) {
|
||||
setLocalSettings(action.payload.data);
|
||||
} else {
|
||||
createToast({ body: "Failed to get settings" });
|
||||
}
|
||||
};
|
||||
getSettings();
|
||||
}, [authToken, dispatch]);
|
||||
|
||||
const logItems = [
|
||||
{ _id: 1, name: "none" },
|
||||
{ _id: 2, name: "debug" },
|
||||
{ _id: 3, name: "error" },
|
||||
{ _id: 4, name: "warn" },
|
||||
];
|
||||
const logItems = [
|
||||
{ _id: 1, name: "none" },
|
||||
{ _id: 2, name: "debug" },
|
||||
{ _id: 3, name: "error" },
|
||||
{ _id: 4, name: "warn" },
|
||||
];
|
||||
|
||||
const logItemLookup = {
|
||||
none: 1,
|
||||
debug: 2,
|
||||
error: 3,
|
||||
warn: 4,
|
||||
};
|
||||
const logItemLookup = {
|
||||
none: 1,
|
||||
debug: 2,
|
||||
error: 3,
|
||||
warn: 4,
|
||||
};
|
||||
|
||||
const handleLogLevel = (e) => {
|
||||
const id = e.target.value;
|
||||
const newLogLevel = logItems.find((item) => item._id === id).name;
|
||||
setLocalSettings({ ...localSettings, logLevel: newLogLevel });
|
||||
};
|
||||
const handleLogLevel = (e) => {
|
||||
const id = e.target.value;
|
||||
const newLogLevel = logItems.find((item) => item._id === id).name;
|
||||
setLocalSettings({ ...localSettings, logLevel: newLogLevel });
|
||||
};
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
setLocalSettings({ ...localSettings, [id]: value });
|
||||
};
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
setLocalSettings({ ...localSettings, [id]: value });
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const action = await dispatch(
|
||||
updateAppSettings({ settings: localSettings, authToken })
|
||||
);
|
||||
let body = "";
|
||||
if (action.payload.success) {
|
||||
console.log(action.payload.data);
|
||||
setLocalSettings(action.payload.data);
|
||||
body = "Settings saved successfully";
|
||||
} else {
|
||||
body = "Failed to save settings";
|
||||
}
|
||||
createToast({ body });
|
||||
};
|
||||
const handleSave = async () => {
|
||||
const action = await dispatch(
|
||||
updateAppSettings({ settings: localSettings, authToken })
|
||||
);
|
||||
let body = "";
|
||||
if (action.payload.success) {
|
||||
console.log(action.payload.data);
|
||||
setLocalSettings(action.payload.data);
|
||||
body = "Settings saved successfully";
|
||||
} else {
|
||||
body = "Failed to save settings";
|
||||
}
|
||||
createToast({ body });
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="settings"
|
||||
style={{
|
||||
paddingBottom: 0,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
component="form"
|
||||
gap={theme.spacing(12)}
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Client settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Modify client settings here.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
id="apiBaseUrl"
|
||||
label="API URL Host"
|
||||
value={localSettings.apiBaseUrl}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Select
|
||||
id="logLevel"
|
||||
label="Log level"
|
||||
name="logLevel"
|
||||
items={logItems}
|
||||
value={logItemLookup[localSettings.logLevel]}
|
||||
onChange={handleLogLevel}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Email settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Set your host email settings here. These settings are used for
|
||||
sending system emails.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
type="text"
|
||||
id="systemEmailHost"
|
||||
label="Email host"
|
||||
name="systemEmailHost"
|
||||
value={localSettings.systemEmailHost}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="number"
|
||||
id="systemEmailPort"
|
||||
label="System email address"
|
||||
name="systemEmailPort"
|
||||
value={localSettings.systemEmailPort.toString()}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="email"
|
||||
id="systemEmailAddress"
|
||||
label="System email address"
|
||||
name="systemEmailAddress"
|
||||
value={localSettings.systemEmailAddress}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="systemEmailPassword"
|
||||
label="System email password"
|
||||
name="systemEmailPassword"
|
||||
value={localSettings.systemEmailPassword}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Server settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Modify server settings here.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
type="text"
|
||||
id="jwtTTL"
|
||||
label="JWT time to live"
|
||||
name="jwtTTL"
|
||||
value={localSettings.jwtTTL}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="dbType"
|
||||
label="Database type"
|
||||
name="dbType"
|
||||
value={localSettings.dbType}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="redisHost"
|
||||
label="Redis host"
|
||||
name="redisHost"
|
||||
value={localSettings.redisHost}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="number"
|
||||
id="redisPort"
|
||||
label="Redis port"
|
||||
name="redisPort"
|
||||
value={localSettings.redisPort.toString()}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="pagespeedApiKey"
|
||||
label="PageSpeed API key"
|
||||
name="pagespeedApiKey"
|
||||
value={localSettings.pagespeedApiKey}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">About</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography component="h2">BlueWave Uptime v1.0.0</Typography>
|
||||
<Typography
|
||||
sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}
|
||||
>
|
||||
Developed by Bluewave Labs.
|
||||
</Typography>
|
||||
<Link
|
||||
level="secondary"
|
||||
url="https://github.com/bluewave-labs"
|
||||
label="https://github.com/bluewave-labs"
|
||||
/>
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<LoadingButton
|
||||
loading={settings.isLoading || settings.authIsLoading}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
return (
|
||||
<Box
|
||||
className="settings"
|
||||
style={{
|
||||
paddingBottom: 0,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
component="form"
|
||||
gap={theme.spacing(12)}
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Client settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Modify client settings here.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
id="apiBaseUrl"
|
||||
label="API URL Host"
|
||||
value={localSettings.apiBaseUrl}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Select
|
||||
id="logLevel"
|
||||
label="Log level"
|
||||
name="logLevel"
|
||||
items={logItems}
|
||||
value={logItemLookup[localSettings.logLevel]}
|
||||
onChange={handleLogLevel}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Email settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Set your host email settings here. These settings are used for sending
|
||||
system emails.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
type="text"
|
||||
id="systemEmailHost"
|
||||
label="Email host"
|
||||
name="systemEmailHost"
|
||||
value={localSettings.systemEmailHost}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="number"
|
||||
id="systemEmailPort"
|
||||
label="System email address"
|
||||
name="systemEmailPort"
|
||||
value={localSettings.systemEmailPort.toString()}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="email"
|
||||
id="systemEmailAddress"
|
||||
label="System email address"
|
||||
name="systemEmailAddress"
|
||||
value={localSettings.systemEmailAddress}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="systemEmailPassword"
|
||||
label="System email password"
|
||||
name="systemEmailPassword"
|
||||
value={localSettings.systemEmailPassword}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Server settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Modify server settings here.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
type="text"
|
||||
id="jwtTTL"
|
||||
label="JWT time to live"
|
||||
name="jwtTTL"
|
||||
value={localSettings.jwtTTL}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="dbType"
|
||||
label="Database type"
|
||||
name="dbType"
|
||||
value={localSettings.dbType}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="redisHost"
|
||||
label="Redis host"
|
||||
name="redisHost"
|
||||
value={localSettings.redisHost}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="number"
|
||||
id="redisPort"
|
||||
label="Redis port"
|
||||
name="redisPort"
|
||||
value={localSettings.redisPort.toString()}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="pagespeedApiKey"
|
||||
label="PageSpeed API key"
|
||||
name="pagespeedApiKey"
|
||||
value={localSettings.pagespeedApiKey}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">About</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography component="h2">BlueWave Uptime v1.0.0</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}>
|
||||
Developed by Bluewave Labs.
|
||||
</Typography>
|
||||
<Link
|
||||
level="secondary"
|
||||
url="https://github.com/bluewave-labs"
|
||||
label="https://github.com/bluewave-labs"
|
||||
/>
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<LoadingButton
|
||||
loading={settings.isLoading || settings.authIsLoading}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
AdvancedSettings.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
isAdmin: PropTypes.bool,
|
||||
};
|
||||
export default AdvancedSettings;
|
||||
|
||||
@@ -12,192 +12,198 @@ import Logo from "../../assets/icons/bwu-icon.svg?react";
|
||||
import "./index.css";
|
||||
|
||||
const CheckEmail = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [email, setEmail] = useState();
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
useEffect(() => {
|
||||
setEmail(sessionStorage.getItem("email"));
|
||||
}, []);
|
||||
const [email, setEmail] = useState();
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
useEffect(() => {
|
||||
setEmail(sessionStorage.getItem("email"));
|
||||
}, []);
|
||||
|
||||
// TODO - fix
|
||||
const openMail = () => {
|
||||
window.location.href = "mailto:";
|
||||
};
|
||||
// TODO - fix
|
||||
const openMail = () => {
|
||||
window.location.href = "mailto:";
|
||||
};
|
||||
|
||||
const toastFail = [
|
||||
{
|
||||
body: "Email not found.",
|
||||
},
|
||||
{
|
||||
body: "Redirecting in 3...",
|
||||
},
|
||||
{
|
||||
body: "Redirecting in 2...",
|
||||
},
|
||||
{
|
||||
body: "Redirecting in 1...",
|
||||
},
|
||||
];
|
||||
const toastFail = [
|
||||
{
|
||||
body: "Email not found.",
|
||||
},
|
||||
{
|
||||
body: "Redirecting in 3...",
|
||||
},
|
||||
{
|
||||
body: "Redirecting in 2...",
|
||||
},
|
||||
{
|
||||
body: "Redirecting in 1...",
|
||||
},
|
||||
];
|
||||
|
||||
const resendToken = async () => {
|
||||
setDisabled(true); // prevent resent button from being spammed
|
||||
if (!email) {
|
||||
let index = 0;
|
||||
const interval = setInterval(() => {
|
||||
if (index < toastFail.length) {
|
||||
createToast(toastFail[index]);
|
||||
index++;
|
||||
} else {
|
||||
clearInterval(interval);
|
||||
navigate("/forgot-password");
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
const form = { email: email };
|
||||
const action = await dispatch(forgotPassword(form));
|
||||
if (action.payload.success) {
|
||||
createToast({
|
||||
body: `Instructions sent to ${form.email}.`,
|
||||
});
|
||||
setDisabled(false);
|
||||
} else {
|
||||
if (action.payload) {
|
||||
// dispatch errors
|
||||
createToast({
|
||||
body: action.payload.msg,
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
body: "Unknown error.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const resendToken = async () => {
|
||||
setDisabled(true); // prevent resent button from being spammed
|
||||
if (!email) {
|
||||
let index = 0;
|
||||
const interval = setInterval(() => {
|
||||
if (index < toastFail.length) {
|
||||
createToast(toastFail[index]);
|
||||
index++;
|
||||
} else {
|
||||
clearInterval(interval);
|
||||
navigate("/forgot-password");
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
const form = { email: email };
|
||||
const action = await dispatch(forgotPassword(form));
|
||||
if (action.payload.success) {
|
||||
createToast({
|
||||
body: `Instructions sent to ${form.email}.`,
|
||||
});
|
||||
setDisabled(false);
|
||||
} else {
|
||||
if (action.payload) {
|
||||
// dispatch errors
|
||||
createToast({
|
||||
body: action.payload.msg,
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
body: "Unknown error.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleNavigate = () => {
|
||||
sessionStorage.removeItem("email");
|
||||
navigate("/login");
|
||||
};
|
||||
const handleNavigate = () => {
|
||||
sessionStorage.removeItem("email");
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className="check-email-page auth"
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 600,
|
||||
fontSize: 22,
|
||||
},
|
||||
"& p": { color: theme.palette.text.accent, fontSize: 13.5 },
|
||||
"& span": { fontSize: "inherit" },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
sx={{
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.border.light,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
px={theme.spacing(12)}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
|
||||
<Typography sx={{ userSelect: "none" }}>BlueWave Uptime</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
width="100%"
|
||||
maxWidth={600}
|
||||
flex={1}
|
||||
justifyContent="center"
|
||||
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
|
||||
pb={theme.spacing(20)}
|
||||
mx="auto"
|
||||
sx={{
|
||||
"& > .MuiStack-root": {
|
||||
border: 1,
|
||||
borderRadius: theme.spacing(5),
|
||||
borderColor: theme.palette.border.light,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
padding: {
|
||||
xs: theme.spacing(12),
|
||||
sm: theme.spacing(20),
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(10) }}
|
||||
alignItems="center"
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<IconBox>
|
||||
<EmailIcon alt="email icon" />
|
||||
</IconBox>
|
||||
<Typography component="h1">Check your email</Typography>
|
||||
<Typography>
|
||||
We sent a password reset link to{" "}
|
||||
<Typography className="email-sent-to" component="span">
|
||||
{email || "username@email.com"}
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={openMail}
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxWidth: 400,
|
||||
}}
|
||||
>
|
||||
Open email app
|
||||
</Button>
|
||||
<Typography sx={{ alignSelf: "center", mt: theme.spacing(6) }}>
|
||||
Didn't receive the email?{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
onClick={resendToken}
|
||||
sx={{
|
||||
color: theme.palette.primary.main,
|
||||
userSelect: "none",
|
||||
pointerEvents: disabled ? "none" : "auto",
|
||||
cursor: disabled ? "default" : "pointer",
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
Click to resend
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Box textAlign="center" p={theme.spacing(12)}>
|
||||
<Typography display="inline-block">Go back to —</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
ml={theme.spacing(2)}
|
||||
color={theme.palette.primary.main}
|
||||
onClick={handleNavigate}
|
||||
sx={{ userSelect: "none" }}
|
||||
>
|
||||
Log In
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
className="check-email-page auth"
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 600,
|
||||
fontSize: 22,
|
||||
},
|
||||
"& p": { color: theme.palette.text.accent, fontSize: 13.5 },
|
||||
"& span": { fontSize: "inherit" },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
sx={{
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.border.light,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
px={theme.spacing(12)}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
|
||||
<Typography sx={{ userSelect: "none" }}>BlueWave Uptime</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
width="100%"
|
||||
maxWidth={600}
|
||||
flex={1}
|
||||
justifyContent="center"
|
||||
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
|
||||
pb={theme.spacing(20)}
|
||||
mx="auto"
|
||||
sx={{
|
||||
"& > .MuiStack-root": {
|
||||
border: 1,
|
||||
borderRadius: theme.spacing(5),
|
||||
borderColor: theme.palette.border.light,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
padding: {
|
||||
xs: theme.spacing(12),
|
||||
sm: theme.spacing(20),
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(10) }}
|
||||
alignItems="center"
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<IconBox>
|
||||
<EmailIcon alt="email icon" />
|
||||
</IconBox>
|
||||
<Typography component="h1">Check your email</Typography>
|
||||
<Typography>
|
||||
We sent a password reset link to{" "}
|
||||
<Typography
|
||||
className="email-sent-to"
|
||||
component="span"
|
||||
>
|
||||
{email || "username@email.com"}
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={openMail}
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxWidth: 400,
|
||||
}}
|
||||
>
|
||||
Open email app
|
||||
</Button>
|
||||
<Typography sx={{ alignSelf: "center", mt: theme.spacing(6) }}>
|
||||
Didn't receive the email?{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
onClick={resendToken}
|
||||
sx={{
|
||||
color: theme.palette.primary.main,
|
||||
userSelect: "none",
|
||||
pointerEvents: disabled ? "none" : "auto",
|
||||
cursor: disabled ? "default" : "pointer",
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
Click to resend
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Box
|
||||
textAlign="center"
|
||||
p={theme.spacing(12)}
|
||||
>
|
||||
<Typography display="inline-block">Go back to —</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
ml={theme.spacing(2)}
|
||||
color={theme.palette.primary.main}
|
||||
onClick={handleNavigate}
|
||||
sx={{ userSelect: "none" }}
|
||||
>
|
||||
Log In
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckEmail;
|
||||
|
||||
@@ -15,196 +15,194 @@ import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import "./index.css";
|
||||
|
||||
const ForgotPassword = () => {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
|
||||
const { isLoading } = useSelector((state) => state.auth);
|
||||
const [errors, setErrors] = useState({});
|
||||
const [form, setForm] = useState({
|
||||
email: "",
|
||||
});
|
||||
const { isLoading } = useSelector((state) => state.auth);
|
||||
const [errors, setErrors] = useState({});
|
||||
const [form, setForm] = useState({
|
||||
email: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const email = sessionStorage.getItem("email");
|
||||
email && setForm({ email: sessionStorage.getItem("email") });
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const email = sessionStorage.getItem("email");
|
||||
email && setForm({ email: sessionStorage.getItem("email") });
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const { error } = credentials.validate(form, { abortEarly: false });
|
||||
const { error } = credentials.validate(form, { abortEarly: false });
|
||||
|
||||
if (error) {
|
||||
// validation errors
|
||||
const err =
|
||||
error.details && error.details.length > 0
|
||||
? error.details[0].message
|
||||
: "Error validating data.";
|
||||
setErrors({ email: err });
|
||||
createToast({
|
||||
body: err,
|
||||
});
|
||||
} else {
|
||||
const action = await dispatch(forgotPassword(form));
|
||||
if (action.payload.success) {
|
||||
sessionStorage.setItem("email", form.email);
|
||||
navigate("/check-email");
|
||||
createToast({
|
||||
body: `Instructions sent to ${form.email}.`,
|
||||
});
|
||||
} else {
|
||||
if (action.payload) {
|
||||
// dispatch errors
|
||||
createToast({
|
||||
body: action.payload.msg,
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
body: "Unknown error.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if (error) {
|
||||
// validation errors
|
||||
const err =
|
||||
error.details && error.details.length > 0
|
||||
? error.details[0].message
|
||||
: "Error validating data.";
|
||||
setErrors({ email: err });
|
||||
createToast({
|
||||
body: err,
|
||||
});
|
||||
} else {
|
||||
const action = await dispatch(forgotPassword(form));
|
||||
if (action.payload.success) {
|
||||
sessionStorage.setItem("email", form.email);
|
||||
navigate("/check-email");
|
||||
createToast({
|
||||
body: `Instructions sent to ${form.email}.`,
|
||||
});
|
||||
} else {
|
||||
if (action.payload) {
|
||||
// dispatch errors
|
||||
createToast({
|
||||
body: action.payload.msg,
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
body: "Unknown error.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value } = event.target;
|
||||
setForm({ email: value });
|
||||
const handleChange = (event) => {
|
||||
const { value } = event.target;
|
||||
setForm({ email: value });
|
||||
|
||||
const { error } = credentials.validate(
|
||||
{ email: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
const { error } = credentials.validate({ email: value }, { abortEarly: false });
|
||||
|
||||
if (error) setErrors({ email: error.details[0].message });
|
||||
else delete errors.email;
|
||||
};
|
||||
if (error) setErrors({ email: error.details[0].message });
|
||||
else delete errors.email;
|
||||
};
|
||||
|
||||
const handleNavigate = () => {
|
||||
sessionStorage.removeItem("email");
|
||||
navigate("/login");
|
||||
};
|
||||
const handleNavigate = () => {
|
||||
sessionStorage.removeItem("email");
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className="forgot-password-page auth"
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 600,
|
||||
fontSize: 21,
|
||||
},
|
||||
"& p": {
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.accent,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
sx={{
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.border.light,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
px={theme.spacing(12)}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
|
||||
<Typography sx={{ userSelect: "none" }}>BlueWave Uptime</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
width="100%"
|
||||
maxWidth={600}
|
||||
flex={1}
|
||||
justifyContent="center"
|
||||
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
|
||||
pb={theme.spacing(20)}
|
||||
mx="auto"
|
||||
sx={{
|
||||
"& > .MuiStack-root": {
|
||||
border: 1,
|
||||
borderRadius: theme.spacing(5),
|
||||
borderColor: theme.palette.border.light,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
padding: {
|
||||
xs: theme.spacing(12),
|
||||
sm: theme.spacing(20),
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
alignItems="center"
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<IconBox>
|
||||
<Key alt="password key icon" />
|
||||
</IconBox>
|
||||
<Typography component="h1">Forgot password?</Typography>
|
||||
<Typography>
|
||||
No worries, we'll send you reset instructions.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
component="form"
|
||||
width="95%"
|
||||
textAlign="left"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Field
|
||||
type="email"
|
||||
id="forgot-password-email-input"
|
||||
label="Email"
|
||||
isRequired={true}
|
||||
placeholder="Enter your email"
|
||||
value={form.email}
|
||||
onChange={handleChange}
|
||||
error={errors.email}
|
||||
/>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
loading={isLoading}
|
||||
disabled={errors.email !== undefined}
|
||||
onClick={handleSubmit}
|
||||
sx={{
|
||||
width: "100%",
|
||||
mt: theme.spacing(15),
|
||||
}}
|
||||
>
|
||||
Send instructions
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Box textAlign="center" p={theme.spacing(12)}>
|
||||
<Typography display="inline-block">Go back to —</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
color={theme.palette.primary.main}
|
||||
ml={theme.spacing(2)}
|
||||
onClick={handleNavigate}
|
||||
sx={{ userSelect: "none" }}
|
||||
>
|
||||
Log In
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
className="forgot-password-page auth"
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 600,
|
||||
fontSize: 21,
|
||||
},
|
||||
"& p": {
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.accent,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
sx={{
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.border.light,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
px={theme.spacing(12)}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
|
||||
<Typography sx={{ userSelect: "none" }}>BlueWave Uptime</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
width="100%"
|
||||
maxWidth={600}
|
||||
flex={1}
|
||||
justifyContent="center"
|
||||
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
|
||||
pb={theme.spacing(20)}
|
||||
mx="auto"
|
||||
sx={{
|
||||
"& > .MuiStack-root": {
|
||||
border: 1,
|
||||
borderRadius: theme.spacing(5),
|
||||
borderColor: theme.palette.border.light,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
padding: {
|
||||
xs: theme.spacing(12),
|
||||
sm: theme.spacing(20),
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
alignItems="center"
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<IconBox>
|
||||
<Key alt="password key icon" />
|
||||
</IconBox>
|
||||
<Typography component="h1">Forgot password?</Typography>
|
||||
<Typography>No worries, we'll send you reset instructions.</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
component="form"
|
||||
width="95%"
|
||||
textAlign="left"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Field
|
||||
type="email"
|
||||
id="forgot-password-email-input"
|
||||
label="Email"
|
||||
isRequired={true}
|
||||
placeholder="Enter your email"
|
||||
value={form.email}
|
||||
onChange={handleChange}
|
||||
error={errors.email}
|
||||
/>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
loading={isLoading}
|
||||
disabled={errors.email !== undefined}
|
||||
onClick={handleSubmit}
|
||||
sx={{
|
||||
width: "100%",
|
||||
mt: theme.spacing(15),
|
||||
}}
|
||||
>
|
||||
Send instructions
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Box
|
||||
textAlign="center"
|
||||
p={theme.spacing(12)}
|
||||
>
|
||||
<Typography display="inline-block">Go back to —</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
color={theme.palette.primary.main}
|
||||
ml={theme.spacing(2)}
|
||||
onClick={handleNavigate}
|
||||
sx={{ userSelect: "none" }}
|
||||
>
|
||||
Log In
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForgotPassword;
|
||||
|
||||
@@ -25,85 +25,85 @@ const DEMO = import.meta.env.VITE_APP_DEMO;
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const LandingPage = ({ onContinue }) => {
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
alignItems="center"
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Log In</Typography>
|
||||
<Typography>We are pleased to see you again!</Typography>
|
||||
</Box>
|
||||
<Box width="100%">
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onContinue}
|
||||
sx={{
|
||||
width: "100%",
|
||||
"& svg": {
|
||||
mr: theme.spacing(4),
|
||||
"& path": {
|
||||
stroke: theme.palette.other.icon,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Mail />
|
||||
Continue with Email
|
||||
</Button>
|
||||
</Box>
|
||||
<Box maxWidth={400}>
|
||||
<Typography className="tos-p">
|
||||
By continuing, you agree to our{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
onClick={() => {
|
||||
window.open(
|
||||
"https://bluewavelabs.ca/terms-of-service-open-source",
|
||||
"_blank",
|
||||
"noreferrer"
|
||||
);
|
||||
}}
|
||||
sx={{
|
||||
"&:hover": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Terms of Service
|
||||
</Typography>{" "}
|
||||
and{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
onClick={() => {
|
||||
window.open(
|
||||
"https://bluewavelabs.ca/privacy-policy-open-source",
|
||||
"_blank",
|
||||
"noreferrer"
|
||||
);
|
||||
}}
|
||||
sx={{
|
||||
"&:hover": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Privacy Policy.
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
alignItems="center"
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Log In</Typography>
|
||||
<Typography>We are pleased to see you again!</Typography>
|
||||
</Box>
|
||||
<Box width="100%">
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onContinue}
|
||||
sx={{
|
||||
width: "100%",
|
||||
"& svg": {
|
||||
mr: theme.spacing(4),
|
||||
"& path": {
|
||||
stroke: theme.palette.other.icon,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Mail />
|
||||
Continue with Email
|
||||
</Button>
|
||||
</Box>
|
||||
<Box maxWidth={400}>
|
||||
<Typography className="tos-p">
|
||||
By continuing, you agree to our{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
onClick={() => {
|
||||
window.open(
|
||||
"https://bluewavelabs.ca/terms-of-service-open-source",
|
||||
"_blank",
|
||||
"noreferrer"
|
||||
);
|
||||
}}
|
||||
sx={{
|
||||
"&:hover": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Terms of Service
|
||||
</Typography>{" "}
|
||||
and{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
onClick={() => {
|
||||
window.open(
|
||||
"https://bluewavelabs.ca/privacy-policy-open-source",
|
||||
"_blank",
|
||||
"noreferrer"
|
||||
);
|
||||
}}
|
||||
sx={{
|
||||
"&:hover": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Privacy Policy.
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
LandingPage.propTypes = {
|
||||
onContinue: PropTypes.func.isRequired,
|
||||
onContinue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -118,79 +118,89 @@ LandingPage.propTypes = {
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
const theme = useTheme();
|
||||
const inputRef = useRef(null);
|
||||
const theme = useTheme();
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Log In</Typography>
|
||||
<Typography>Enter your email address</Typography>
|
||||
</Box>
|
||||
<Box textAlign="left" mb={theme.spacing(5)}>
|
||||
<form noValidate spellCheck={false} onSubmit={onSubmit}>
|
||||
<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>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Log In</Typography>
|
||||
<Typography>Enter your email address</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
textAlign="left"
|
||||
mb={theme.spacing(5)}
|
||||
>
|
||||
<form
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<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>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
StepOne.propTypes = {
|
||||
form: PropTypes.object.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onBack: PropTypes.func.isRequired,
|
||||
form: PropTypes.object.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onBack: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -205,326 +215,333 @@ StepOne.propTypes = {
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const inputRef = useRef(null);
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleNavigate = () => {
|
||||
if (form.email !== "" && !errors.email) {
|
||||
sessionStorage.setItem("email", form.email);
|
||||
}
|
||||
navigate("/forgot-password");
|
||||
};
|
||||
const handleNavigate = () => {
|
||||
if (form.email !== "" && !errors.email) {
|
||||
sessionStorage.setItem("email", form.email);
|
||||
}
|
||||
navigate("/forgot-password");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
position="relative"
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Log In</Typography>
|
||||
<Typography>Enter your password</Typography>
|
||||
</Box>
|
||||
<Box textAlign="left" mb={theme.spacing(5)}>
|
||||
<form noValidate spellCheck={false} onSubmit={onSubmit}>
|
||||
<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>
|
||||
</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%",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
className="forgot-p"
|
||||
display="inline-block"
|
||||
color={theme.palette.primary.main}
|
||||
>
|
||||
Forgot password?
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
color={theme.palette.primary.main}
|
||||
ml={theme.spacing(2)}
|
||||
sx={{ userSelect: "none" }}
|
||||
onClick={handleNavigate}
|
||||
>
|
||||
Reset password
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
position="relative"
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Log In</Typography>
|
||||
<Typography>Enter your password</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
textAlign="left"
|
||||
mb={theme.spacing(5)}
|
||||
>
|
||||
<form
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<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>
|
||||
</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%",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
className="forgot-p"
|
||||
display="inline-block"
|
||||
color={theme.palette.primary.main}
|
||||
>
|
||||
Forgot password?
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
color={theme.palette.primary.main}
|
||||
ml={theme.spacing(2)}
|
||||
sx={{ userSelect: "none" }}
|
||||
onClick={handleNavigate}
|
||||
>
|
||||
Reset password
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
StepTwo.propTypes = {
|
||||
form: PropTypes.object.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onBack: PropTypes.func.isRequired,
|
||||
form: PropTypes.object.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onBack: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const Login = () => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const { authToken } = authState;
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const { authToken } = authState;
|
||||
|
||||
const idMap = {
|
||||
"login-email-input": "email",
|
||||
"login-password-input": "password",
|
||||
};
|
||||
const idMap = {
|
||||
"login-email-input": "email",
|
||||
"login-password-input": "password",
|
||||
};
|
||||
|
||||
const [form, setForm] = useState({
|
||||
email: DEMO !== undefined ? "uptimedemo@demo.com" : "",
|
||||
password: DEMO !== undefined ? "Demouser1!" : "",
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [step, setStep] = useState(0);
|
||||
const [form, setForm] = useState({
|
||||
email: DEMO !== undefined ? "uptimedemo@demo.com" : "",
|
||||
password: DEMO !== undefined ? "Demouser1!" : "",
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [step, setStep] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (authToken) {
|
||||
navigate("/monitors");
|
||||
return;
|
||||
}
|
||||
networkService
|
||||
.doesSuperAdminExist()
|
||||
.then((response) => {
|
||||
if (response.data.data === false) {
|
||||
navigate("/register");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(error);
|
||||
});
|
||||
}, [authToken, navigate]);
|
||||
useEffect(() => {
|
||||
if (authToken) {
|
||||
navigate("/monitors");
|
||||
return;
|
||||
}
|
||||
networkService
|
||||
.doesSuperAdminExist()
|
||||
.then((response) => {
|
||||
if (response.data.data === false) {
|
||||
navigate("/register");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(error);
|
||||
});
|
||||
}, [authToken, navigate]);
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
const name = idMap[id];
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
const name = idMap[id];
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
|
||||
const { error } = credentials.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
const { error } = credentials.validate({ [name]: value }, { abortEarly: false });
|
||||
|
||||
setErrors((prev) => {
|
||||
const prevErrors = { ...prev };
|
||||
if (error) prevErrors[name] = error.details[0].message;
|
||||
else delete prevErrors[name];
|
||||
return prevErrors;
|
||||
});
|
||||
};
|
||||
setErrors((prev) => {
|
||||
const prevErrors = { ...prev };
|
||||
if (error) prevErrors[name] = error.details[0].message;
|
||||
else delete prevErrors[name];
|
||||
return prevErrors;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (step === 1) {
|
||||
const { error } = credentials.validate(
|
||||
{ email: form.email },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
if (error) {
|
||||
setErrors((prev) => ({ ...prev, email: error.details[0].message }));
|
||||
createToast({ body: error.details[0].message });
|
||||
} else {
|
||||
setStep(2);
|
||||
}
|
||||
} else if (step === 2) {
|
||||
const { error } = credentials.validate(form, { abortEarly: false });
|
||||
if (step === 1) {
|
||||
const { error } = credentials.validate(
|
||||
{ email: form.email },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
if (error) {
|
||||
setErrors((prev) => ({ ...prev, email: error.details[0].message }));
|
||||
createToast({ body: error.details[0].message });
|
||||
} else {
|
||||
setStep(2);
|
||||
}
|
||||
} else if (step === 2) {
|
||||
const { error } = credentials.validate(form, { abortEarly: false });
|
||||
|
||||
if (error) {
|
||||
// validation errors
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
createToast({
|
||||
body:
|
||||
error.details && error.details.length > 0
|
||||
? error.details[0].message
|
||||
: "Error validating data.",
|
||||
});
|
||||
} else {
|
||||
const action = await dispatch(login(form));
|
||||
if (action.payload.success) {
|
||||
navigate("/monitors");
|
||||
createToast({
|
||||
body: "Welcome back! You're successfully logged in.",
|
||||
});
|
||||
} else {
|
||||
if (action.payload) {
|
||||
if (action.payload.msg === "Incorrect password")
|
||||
setErrors({
|
||||
password:
|
||||
"The password you provided does not match our records",
|
||||
});
|
||||
// dispatch errors
|
||||
createToast({
|
||||
body: action.payload.msg,
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
body: "Unknown error.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if (error) {
|
||||
// validation errors
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
createToast({
|
||||
body:
|
||||
error.details && error.details.length > 0
|
||||
? error.details[0].message
|
||||
: "Error validating data.",
|
||||
});
|
||||
} else {
|
||||
const action = await dispatch(login(form));
|
||||
if (action.payload.success) {
|
||||
navigate("/monitors");
|
||||
createToast({
|
||||
body: "Welcome back! You're successfully logged in.",
|
||||
});
|
||||
} else {
|
||||
if (action.payload) {
|
||||
if (action.payload.msg === "Incorrect password")
|
||||
setErrors({
|
||||
password: "The password you provided does not match our records",
|
||||
});
|
||||
// dispatch errors
|
||||
createToast({
|
||||
body: action.payload.msg,
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
body: "Unknown error.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className="login-page auth"
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 600,
|
||||
fontSize: 28,
|
||||
},
|
||||
"& p": { fontSize: 14, color: theme.palette.text.accent },
|
||||
"& span": { fontSize: "inherit" },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
sx={{
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.border.light,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
px={theme.spacing(12)}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
|
||||
<Typography sx={{ userSelect: "none" }}>BlueWave Uptime</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
width="100%"
|
||||
maxWidth={600}
|
||||
flex={1}
|
||||
justifyContent="center"
|
||||
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
|
||||
pb={theme.spacing(20)}
|
||||
mx="auto"
|
||||
sx={{
|
||||
"& > .MuiStack-root": {
|
||||
border: 1,
|
||||
borderRadius: theme.spacing(5),
|
||||
borderColor: theme.palette.border.light,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
padding: {
|
||||
xs: theme.spacing(12),
|
||||
sm: theme.spacing(20),
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{step === 0 ? (
|
||||
<LandingPage onContinue={() => setStep(1)} />
|
||||
) : step === 1 ? (
|
||||
<StepOne
|
||||
form={form}
|
||||
errors={errors}
|
||||
onSubmit={handleSubmit}
|
||||
onChange={handleChange}
|
||||
onBack={() => setStep(0)}
|
||||
/>
|
||||
) : (
|
||||
step === 2 && (
|
||||
<StepTwo
|
||||
form={form}
|
||||
errors={errors}
|
||||
onSubmit={handleSubmit}
|
||||
onChange={handleChange}
|
||||
onBack={() => setStep(1)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
<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>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
className="login-page auth"
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 600,
|
||||
fontSize: 28,
|
||||
},
|
||||
"& p": { fontSize: 14, color: theme.palette.text.accent },
|
||||
"& span": { fontSize: "inherit" },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
sx={{
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.border.light,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
px={theme.spacing(12)}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
|
||||
<Typography sx={{ userSelect: "none" }}>BlueWave Uptime</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
width="100%"
|
||||
maxWidth={600}
|
||||
flex={1}
|
||||
justifyContent="center"
|
||||
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
|
||||
pb={theme.spacing(20)}
|
||||
mx="auto"
|
||||
sx={{
|
||||
"& > .MuiStack-root": {
|
||||
border: 1,
|
||||
borderRadius: theme.spacing(5),
|
||||
borderColor: theme.palette.border.light,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
padding: {
|
||||
xs: theme.spacing(12),
|
||||
sm: theme.spacing(20),
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{step === 0 ? (
|
||||
<LandingPage onContinue={() => setStep(1)} />
|
||||
) : step === 1 ? (
|
||||
<StepOne
|
||||
form={form}
|
||||
errors={errors}
|
||||
onSubmit={handleSubmit}
|
||||
onChange={handleChange}
|
||||
onBack={() => setStep(0)}
|
||||
/>
|
||||
) : (
|
||||
step === 2 && (
|
||||
<StepTwo
|
||||
form={form}
|
||||
errors={errors}
|
||||
onSubmit={handleSubmit}
|
||||
onChange={handleChange}
|
||||
onBack={() => setStep(1)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
|
||||
@@ -11,111 +11,113 @@ import Logo from "../../assets/icons/bwu-icon.svg?react";
|
||||
import "./index.css";
|
||||
|
||||
const NewPasswordConfirmed = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleNavigate = () => {
|
||||
dispatch(clearAuthState());
|
||||
dispatch(clearUptimeMonitorState());
|
||||
navigate("/login");
|
||||
};
|
||||
const handleNavigate = () => {
|
||||
dispatch(clearAuthState());
|
||||
dispatch(clearUptimeMonitorState());
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className="password-confirmed-page auth"
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 600,
|
||||
fontSize: 21,
|
||||
},
|
||||
"& p": { fontSize: 13.5, color: theme.palette.text.accent },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
sx={{
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.border.light,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
px={theme.spacing(12)}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
|
||||
<Typography sx={{ userSelect: "none" }}>BlueWave Uptime</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
width="100%"
|
||||
maxWidth={600}
|
||||
flex={1}
|
||||
justifyContent="center"
|
||||
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
|
||||
pb={theme.spacing(20)}
|
||||
mx="auto"
|
||||
sx={{
|
||||
"& > .MuiStack-root": {
|
||||
border: 1,
|
||||
borderRadius: theme.spacing(5),
|
||||
borderColor: theme.palette.border.light,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
padding: {
|
||||
xs: theme.spacing(12),
|
||||
sm: theme.spacing(20),
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
alignItems="center"
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<IconBox>
|
||||
<ConfirmIcon alt="password confirm icon" />
|
||||
</IconBox>
|
||||
<Typography component="h1">Password reset</Typography>
|
||||
<Typography mb={theme.spacing(2)}>
|
||||
Your password has been successfully reset. Click below to log in
|
||||
magically.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => navigate("/monitors")}
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxWidth: 400,
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Box textAlign="center" p={theme.spacing(12)}>
|
||||
<Typography display="inline-block">Go back to —</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
color={theme.palette.primary.main}
|
||||
ml={theme.spacing(2)}
|
||||
onClick={handleNavigate}
|
||||
sx={{ userSelect: "none" }}
|
||||
>
|
||||
Log In
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
className="password-confirmed-page auth"
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 600,
|
||||
fontSize: 21,
|
||||
},
|
||||
"& p": { fontSize: 13.5, color: theme.palette.text.accent },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
sx={{
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.border.light,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
px={theme.spacing(12)}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
|
||||
<Typography sx={{ userSelect: "none" }}>BlueWave Uptime</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
width="100%"
|
||||
maxWidth={600}
|
||||
flex={1}
|
||||
justifyContent="center"
|
||||
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
|
||||
pb={theme.spacing(20)}
|
||||
mx="auto"
|
||||
sx={{
|
||||
"& > .MuiStack-root": {
|
||||
border: 1,
|
||||
borderRadius: theme.spacing(5),
|
||||
borderColor: theme.palette.border.light,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
padding: {
|
||||
xs: theme.spacing(12),
|
||||
sm: theme.spacing(20),
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
alignItems="center"
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<IconBox>
|
||||
<ConfirmIcon alt="password confirm icon" />
|
||||
</IconBox>
|
||||
<Typography component="h1">Password reset</Typography>
|
||||
<Typography mb={theme.spacing(2)}>
|
||||
Your password has been successfully reset. Click below to log in magically.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => navigate("/monitors")}
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxWidth: 400,
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Box
|
||||
textAlign="center"
|
||||
p={theme.spacing(12)}
|
||||
>
|
||||
<Typography display="inline-block">Go back to —</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
color={theme.palette.primary.main}
|
||||
ml={theme.spacing(2)}
|
||||
onClick={handleNavigate}
|
||||
sx={{ userSelect: "none" }}
|
||||
>
|
||||
Log In
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewPasswordConfirmed;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,290 +17,290 @@ import "./index.css";
|
||||
import { IconBox } from "./styled";
|
||||
|
||||
const SetNewPassword = () => {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
|
||||
const [errors, setErrors] = useState({});
|
||||
const [form, setForm] = useState({
|
||||
password: "",
|
||||
confirm: "",
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [form, setForm] = useState({
|
||||
password: "",
|
||||
confirm: "",
|
||||
});
|
||||
|
||||
const idMap = {
|
||||
"register-password-input": "password",
|
||||
"confirm-password-input": "confirm",
|
||||
};
|
||||
const idMap = {
|
||||
"register-password-input": "password",
|
||||
"confirm-password-input": "confirm",
|
||||
};
|
||||
|
||||
const { isLoading } = useSelector((state) => state.auth);
|
||||
const { token } = useParams();
|
||||
const { isLoading } = useSelector((state) => state.auth);
|
||||
const { token } = useParams();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const passwordForm = { ...form };
|
||||
const { error } = credentials.validate(passwordForm, {
|
||||
abortEarly: false,
|
||||
context: { password: form.password },
|
||||
});
|
||||
const passwordForm = { ...form };
|
||||
const { error } = credentials.validate(passwordForm, {
|
||||
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
|
||||
? error.details[0].message
|
||||
: "Error validating data.",
|
||||
});
|
||||
} else {
|
||||
delete passwordForm.confirm;
|
||||
const action = await dispatch(
|
||||
setNewPassword({ token: token, form: passwordForm })
|
||||
);
|
||||
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.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if (error) {
|
||||
// validation errors
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
createToast({
|
||||
body:
|
||||
error.details && error.details.length > 0
|
||||
? error.details[0].message
|
||||
: "Error validating data.",
|
||||
});
|
||||
} else {
|
||||
delete passwordForm.confirm;
|
||||
const action = await dispatch(setNewPassword({ token: token, form: passwordForm }));
|
||||
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 handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
const name = idMap[id];
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
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 } }
|
||||
);
|
||||
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;
|
||||
});
|
||||
};
|
||||
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"
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 600,
|
||||
fontSize: 24,
|
||||
},
|
||||
"& p": {
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.accent,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
sx={{
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.border.light,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
px={theme.spacing(12)}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
|
||||
<Typography sx={{ userSelect: "none" }}>BlueWave Uptime</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
width="100%"
|
||||
maxWidth={600}
|
||||
flex={1}
|
||||
justifyContent="center"
|
||||
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
|
||||
pb={theme.spacing(12)}
|
||||
mx="auto"
|
||||
sx={{
|
||||
"& > .MuiStack-root": {
|
||||
border: 1,
|
||||
borderRadius: theme.spacing(5),
|
||||
borderColor: theme.palette.border.light,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
padding: {
|
||||
xs: theme.spacing(12),
|
||||
sm: theme.spacing(20),
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
alignItems="center"
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<IconBox>
|
||||
<LockIcon alt="lock icon" />
|
||||
</IconBox>
|
||||
<Typography component="h1">Set new password</Typography>
|
||||
<Typography>
|
||||
Your new password must be different to previously used passwords.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
width="100%"
|
||||
textAlign="left"
|
||||
sx={{
|
||||
"& .input-error": {
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Field
|
||||
type="password"
|
||||
id="register-password-input"
|
||||
label="Password"
|
||||
isRequired={true}
|
||||
placeholder="••••••••"
|
||||
value={form.password}
|
||||
onChange={handleChange}
|
||||
error={errors.password}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Field
|
||||
type="password"
|
||||
id="confirm-password-input"
|
||||
label="Confirm password"
|
||||
isRequired={true}
|
||||
placeholder="••••••••"
|
||||
value={form.confirm}
|
||||
onChange={handleChange}
|
||||
error={errors.confirm}
|
||||
/>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(4)} 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"
|
||||
}
|
||||
/>
|
||||
<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>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
loading={isLoading}
|
||||
onClick={handleSubmit}
|
||||
disabled={Object.keys(errors).length !== 0}
|
||||
sx={{ width: "100%", maxWidth: 400 }}
|
||||
>
|
||||
Reset password
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Box textAlign="center" p={theme.spacing(12)}>
|
||||
<Typography display="inline-block">Go back to —</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
color={theme.palette.primary.main}
|
||||
ml={theme.spacing(2)}
|
||||
onClick={() => navigate("/login")}
|
||||
sx={{ userSelect: "none" }}
|
||||
>
|
||||
Log In
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
className="set-new-password-page auth"
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 600,
|
||||
fontSize: 24,
|
||||
},
|
||||
"& p": {
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.accent,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
sx={{
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.border.light,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
px={theme.spacing(12)}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
|
||||
<Typography sx={{ userSelect: "none" }}>BlueWave Uptime</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
width="100%"
|
||||
maxWidth={600}
|
||||
flex={1}
|
||||
justifyContent="center"
|
||||
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
|
||||
pb={theme.spacing(12)}
|
||||
mx="auto"
|
||||
sx={{
|
||||
"& > .MuiStack-root": {
|
||||
border: 1,
|
||||
borderRadius: theme.spacing(5),
|
||||
borderColor: theme.palette.border.light,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
padding: {
|
||||
xs: theme.spacing(12),
|
||||
sm: theme.spacing(20),
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
alignItems="center"
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<IconBox>
|
||||
<LockIcon alt="lock icon" />
|
||||
</IconBox>
|
||||
<Typography component="h1">Set new password</Typography>
|
||||
<Typography>
|
||||
Your new password must be different to previously used passwords.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
width="100%"
|
||||
textAlign="left"
|
||||
sx={{
|
||||
"& .input-error": {
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Field
|
||||
type="password"
|
||||
id="register-password-input"
|
||||
label="Password"
|
||||
isRequired={true}
|
||||
placeholder="••••••••"
|
||||
value={form.password}
|
||||
onChange={handleChange}
|
||||
error={errors.password}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Field
|
||||
type="password"
|
||||
id="confirm-password-input"
|
||||
label="Confirm password"
|
||||
isRequired={true}
|
||||
placeholder="••••••••"
|
||||
value={form.confirm}
|
||||
onChange={handleChange}
|
||||
error={errors.confirm}
|
||||
/>
|
||||
</Box>
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
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"
|
||||
}
|
||||
/>
|
||||
<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>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
loading={isLoading}
|
||||
onClick={handleSubmit}
|
||||
disabled={Object.keys(errors).length !== 0}
|
||||
sx={{ width: "100%", maxWidth: 400 }}
|
||||
>
|
||||
Reset password
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Box
|
||||
textAlign="center"
|
||||
p={theme.spacing(12)}
|
||||
>
|
||||
<Typography display="inline-block">Go back to —</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
color={theme.palette.primary.main}
|
||||
ml={theme.spacing(2)}
|
||||
onClick={() => navigate("/login")}
|
||||
sx={{ userSelect: "none" }}
|
||||
>
|
||||
Log In
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetNewPassword;
|
||||
|
||||
@@ -1,120 +1,120 @@
|
||||
/* AUTH */
|
||||
.auth {
|
||||
height: 100vh;
|
||||
height: 100vh;
|
||||
}
|
||||
.auth h1 {
|
||||
font-size: var(--env-var-font-size-xlarge);
|
||||
font-weight: 600;
|
||||
font-size: var(--env-var-font-size-xlarge);
|
||||
font-weight: 600;
|
||||
}
|
||||
.auth button:not(.MuiIconButton-root) {
|
||||
font-size: var(--env-var-font-size-medium-plus);
|
||||
font-size: var(--env-var-font-size-medium-plus);
|
||||
}
|
||||
|
||||
.auth p + span {
|
||||
opacity: 0.8;
|
||||
cursor: pointer;
|
||||
transition: opacity 300ms ease-in;
|
||||
opacity: 0.8;
|
||||
cursor: pointer;
|
||||
transition: opacity 300ms ease-in;
|
||||
}
|
||||
.auth p > span:not(.email-sent-to) {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 200ms;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 200ms;
|
||||
}
|
||||
.auth p > span:not(.email-sent-to):hover {
|
||||
text-underline-offset: 4px;
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
.auth p + span:hover {
|
||||
opacity: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
.auth button:not(.MuiIconButton-root) {
|
||||
user-select: none;
|
||||
border-radius: var(--env-var-radius-2);
|
||||
line-height: 1;
|
||||
user-select: none;
|
||||
border-radius: var(--env-var-radius-2);
|
||||
line-height: 1;
|
||||
}
|
||||
.auth button:not(.MuiIconButton-root),
|
||||
.auth .field .MuiInputBase-root:has(input) {
|
||||
height: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
.auth .field svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.auth .field h3.MuiTypography-root,
|
||||
.auth .field .input-error,
|
||||
.auth .check span.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.auth form + form {
|
||||
margin: var(--env-var-spacing-1-plus) 0;
|
||||
margin: var(--env-var-spacing-1-plus) 0;
|
||||
}
|
||||
.auth .email-sent-to {
|
||||
font-weight: 600;
|
||||
cursor: default;
|
||||
font-weight: 600;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.auth > .MuiStack-root:nth-of-type(2) {
|
||||
height: var(--env-var-nav-bar-height);
|
||||
height: var(--env-var-nav-bar-height);
|
||||
}
|
||||
.auth .background-pattern-svg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -30%);
|
||||
z-index: -1;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -30%);
|
||||
z-index: -1;
|
||||
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
height: 100%;
|
||||
max-height: 800px;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
height: 100%;
|
||||
max-height: 800px;
|
||||
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.auth .field {
|
||||
position: relative;
|
||||
position: relative;
|
||||
}
|
||||
.auth .input-error {
|
||||
position: absolute;
|
||||
top: calc(100% - 2px);
|
||||
position: absolute;
|
||||
top: calc(100% - 2px);
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.auth h1 {
|
||||
font-size: var(--env-var-font-size-large-plus);
|
||||
}
|
||||
.auth button:not(.MuiIconButton-root),
|
||||
.auth .field input,
|
||||
.auth p,
|
||||
.auth span {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.auth button:not(.MuiIconButton-root),
|
||||
.auth .field .MuiInputBase-root:has(input) {
|
||||
height: 36px;
|
||||
}
|
||||
.auth .check span.MuiTypography-root,
|
||||
.auth .field .input-error,
|
||||
.auth .field h3.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
}
|
||||
.auth .check span > span {
|
||||
display: none;
|
||||
}
|
||||
.auth form + form {
|
||||
margin: var(--env-var-spacing-1-plus) 0;
|
||||
}
|
||||
.auth .background-pattern-svg {
|
||||
max-width: 750px;
|
||||
max-height: 750px;
|
||||
}
|
||||
.auth h1 {
|
||||
font-size: var(--env-var-font-size-large-plus);
|
||||
}
|
||||
.auth button:not(.MuiIconButton-root),
|
||||
.auth .field input,
|
||||
.auth p,
|
||||
.auth span {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.auth button:not(.MuiIconButton-root),
|
||||
.auth .field .MuiInputBase-root:has(input) {
|
||||
height: 36px;
|
||||
}
|
||||
.auth .check span.MuiTypography-root,
|
||||
.auth .field .input-error,
|
||||
.auth .field h3.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
}
|
||||
.auth .check span > span {
|
||||
display: none;
|
||||
}
|
||||
.auth form + form {
|
||||
margin: var(--env-var-spacing-1-plus) 0;
|
||||
}
|
||||
.auth .background-pattern-svg {
|
||||
max-width: 750px;
|
||||
max-height: 750px;
|
||||
}
|
||||
|
||||
.forgot-password-page h1,
|
||||
.check-email-page h1,
|
||||
.password-confirmed-page h1,
|
||||
.set-new-password-page h1 {
|
||||
font-size: 18px;
|
||||
}
|
||||
.forgot-password-page h1,
|
||||
.check-email-page h1,
|
||||
.password-confirmed-page h1,
|
||||
.set-new-password-page h1 {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { Box, styled } from "@mui/material";
|
||||
|
||||
export const IconBox = styled(Box)(({ theme }) => ({
|
||||
height: 48,
|
||||
minWidth: 48,
|
||||
width: 48,
|
||||
position: "relative",
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.dark,
|
||||
borderRadius: 12,
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
margin: "auto",
|
||||
marginBottom: 8,
|
||||
"& svg": {
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 24,
|
||||
height: 24,
|
||||
"& path": {
|
||||
stroke: theme.palette.text.tertiary,
|
||||
},
|
||||
},
|
||||
height: 48,
|
||||
minWidth: 48,
|
||||
width: 48,
|
||||
position: "relative",
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.dark,
|
||||
borderRadius: 12,
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
margin: "auto",
|
||||
marginBottom: 8,
|
||||
"& svg": {
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 24,
|
||||
height: 24,
|
||||
"& path": {
|
||||
stroke: theme.palette.text.tertiary,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
TableContainer,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
Pagination,
|
||||
PaginationItem,
|
||||
Paper,
|
||||
Typography,
|
||||
Box,
|
||||
TableContainer,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
Pagination,
|
||||
PaginationItem,
|
||||
Paper,
|
||||
Typography,
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
@@ -26,183 +26,193 @@ import PlaceholderLight from "../../../assets/Images/data_placeholder.svg?react"
|
||||
import PlaceholderDark from "../../../assets/Images/data_placeholder_dark.svg?react";
|
||||
|
||||
const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
const theme = useTheme();
|
||||
const { authToken, user } = useSelector((state) => state.auth);
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
const [checks, setChecks] = useState([]);
|
||||
const [checksCount, setChecksCount] = useState(0);
|
||||
const [paginationController, setPaginationController] = useState({
|
||||
page: 0,
|
||||
rowsPerPage: 14,
|
||||
});
|
||||
const theme = useTheme();
|
||||
const { authToken, user } = useSelector((state) => state.auth);
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
const [checks, setChecks] = useState([]);
|
||||
const [checksCount, setChecksCount] = useState(0);
|
||||
const [paginationController, setPaginationController] = useState({
|
||||
page: 0,
|
||||
rowsPerPage: 14,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setPaginationController((prevPaginationController) => ({
|
||||
...prevPaginationController,
|
||||
page: 0,
|
||||
}));
|
||||
}, [filter, selectedMonitor]);
|
||||
useEffect(() => {
|
||||
setPaginationController((prevPaginationController) => ({
|
||||
...prevPaginationController,
|
||||
page: 0,
|
||||
}));
|
||||
}, [filter, selectedMonitor]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPage = async () => {
|
||||
if (!monitors || Object.keys(monitors).length === 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let res;
|
||||
if (selectedMonitor === "0") {
|
||||
res = await networkService.getChecksByTeam({
|
||||
authToken: authToken,
|
||||
teamId: user.teamId,
|
||||
sortOrder: "desc",
|
||||
limit: null,
|
||||
dateRange: null,
|
||||
filter: filter,
|
||||
page: paginationController.page,
|
||||
rowsPerPage: paginationController.rowsPerPage,
|
||||
});
|
||||
} else {
|
||||
res = await networkService.getChecksByMonitor({
|
||||
authToken: authToken,
|
||||
monitorId: selectedMonitor,
|
||||
sortOrder: "desc",
|
||||
limit: null,
|
||||
dateRange: null,
|
||||
sitler: filter,
|
||||
page: paginationController.page,
|
||||
rowsPerPage: paginationController.rowsPerPage,
|
||||
});
|
||||
}
|
||||
setChecks(res.data.data.checks);
|
||||
setChecksCount(res.data.data.checksCount);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
};
|
||||
fetchPage();
|
||||
}, [
|
||||
authToken,
|
||||
user,
|
||||
monitors,
|
||||
selectedMonitor,
|
||||
filter,
|
||||
paginationController.page,
|
||||
paginationController.rowsPerPage,
|
||||
]);
|
||||
useEffect(() => {
|
||||
const fetchPage = async () => {
|
||||
if (!monitors || Object.keys(monitors).length === 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let res;
|
||||
if (selectedMonitor === "0") {
|
||||
res = await networkService.getChecksByTeam({
|
||||
authToken: authToken,
|
||||
teamId: user.teamId,
|
||||
sortOrder: "desc",
|
||||
limit: null,
|
||||
dateRange: null,
|
||||
filter: filter,
|
||||
page: paginationController.page,
|
||||
rowsPerPage: paginationController.rowsPerPage,
|
||||
});
|
||||
} else {
|
||||
res = await networkService.getChecksByMonitor({
|
||||
authToken: authToken,
|
||||
monitorId: selectedMonitor,
|
||||
sortOrder: "desc",
|
||||
limit: null,
|
||||
dateRange: null,
|
||||
sitler: filter,
|
||||
page: paginationController.page,
|
||||
rowsPerPage: paginationController.rowsPerPage,
|
||||
});
|
||||
}
|
||||
setChecks(res.data.data.checks);
|
||||
setChecksCount(res.data.data.checksCount);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
};
|
||||
fetchPage();
|
||||
}, [
|
||||
authToken,
|
||||
user,
|
||||
monitors,
|
||||
selectedMonitor,
|
||||
filter,
|
||||
paginationController.page,
|
||||
paginationController.rowsPerPage,
|
||||
]);
|
||||
|
||||
const handlePageChange = (_, newPage) => {
|
||||
setPaginationController({
|
||||
...paginationController,
|
||||
page: newPage - 1, // 0-indexed
|
||||
});
|
||||
};
|
||||
const handlePageChange = (_, newPage) => {
|
||||
setPaginationController({
|
||||
...paginationController,
|
||||
page: newPage - 1, // 0-indexed
|
||||
});
|
||||
};
|
||||
|
||||
let paginationComponent = <></>;
|
||||
if (checksCount > paginationController.rowsPerPage) {
|
||||
paginationComponent = (
|
||||
<Pagination
|
||||
count={Math.ceil(checksCount / paginationController.rowsPerPage)}
|
||||
page={paginationController.page + 1} //0-indexed
|
||||
onChange={handlePageChange}
|
||||
shape="rounded"
|
||||
renderItem={(item) => (
|
||||
<PaginationItem
|
||||
slots={{
|
||||
previous: ArrowBackRoundedIcon,
|
||||
next: ArrowForwardRoundedIcon,
|
||||
}}
|
||||
{...item}
|
||||
/>
|
||||
)}
|
||||
sx={{ mt: "auto" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
let paginationComponent = <></>;
|
||||
if (checksCount > paginationController.rowsPerPage) {
|
||||
paginationComponent = (
|
||||
<Pagination
|
||||
count={Math.ceil(checksCount / paginationController.rowsPerPage)}
|
||||
page={paginationController.page + 1} //0-indexed
|
||||
onChange={handlePageChange}
|
||||
shape="rounded"
|
||||
renderItem={(item) => (
|
||||
<PaginationItem
|
||||
slots={{
|
||||
previous: ArrowBackRoundedIcon,
|
||||
next: ArrowForwardRoundedIcon,
|
||||
}}
|
||||
{...item}
|
||||
/>
|
||||
)}
|
||||
sx={{ mt: "auto" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let sharedStyles = {
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
p: theme.spacing(30),
|
||||
};
|
||||
let sharedStyles = {
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
p: theme.spacing(30),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{checks?.length === 0 && selectedMonitor === "0" ? (
|
||||
<Box sx={{ ...sharedStyles }}>
|
||||
<Box textAlign="center" pb={theme.spacing(20)}>
|
||||
{mode === "light" ? <PlaceholderLight /> : <PlaceholderDark />}
|
||||
</Box>
|
||||
<Typography textAlign="center" color={theme.palette.text.secondary}>
|
||||
No incidents recorded yet.
|
||||
</Typography>
|
||||
</Box>
|
||||
) : checks?.length === 0 ? (
|
||||
<Box sx={{ ...sharedStyles }}>
|
||||
<Box textAlign="center" pb={theme.spacing(20)}>
|
||||
{mode === "light" ? <PlaceholderLight /> : <PlaceholderDark />}
|
||||
</Box>
|
||||
<Typography textAlign="center" color={theme.palette.text.secondary}>
|
||||
The monitor you have selected has no recorded incidents yet.
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Monitor Name</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Date & Time</TableCell>
|
||||
<TableCell>Status Code</TableCell>
|
||||
<TableCell>Message</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{checks.map((check) => {
|
||||
const status = check.status === true ? "up" : "down";
|
||||
const formattedDate = formatDateWithTz(
|
||||
check.createdAt,
|
||||
"YYYY-MM-DD HH:mm:ss A",
|
||||
uiTimezone
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{checks?.length === 0 && selectedMonitor === "0" ? (
|
||||
<Box sx={{ ...sharedStyles }}>
|
||||
<Box
|
||||
textAlign="center"
|
||||
pb={theme.spacing(20)}
|
||||
>
|
||||
{mode === "light" ? <PlaceholderLight /> : <PlaceholderDark />}
|
||||
</Box>
|
||||
<Typography
|
||||
textAlign="center"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
No incidents recorded yet.
|
||||
</Typography>
|
||||
</Box>
|
||||
) : checks?.length === 0 ? (
|
||||
<Box sx={{ ...sharedStyles }}>
|
||||
<Box
|
||||
textAlign="center"
|
||||
pb={theme.spacing(20)}
|
||||
>
|
||||
{mode === "light" ? <PlaceholderLight /> : <PlaceholderDark />}
|
||||
</Box>
|
||||
<Typography
|
||||
textAlign="center"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
The monitor you have selected has no recorded incidents yet.
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Monitor Name</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Date & Time</TableCell>
|
||||
<TableCell>Status Code</TableCell>
|
||||
<TableCell>Message</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{checks.map((check) => {
|
||||
const status = check.status === true ? "up" : "down";
|
||||
const formattedDate = formatDateWithTz(
|
||||
check.createdAt,
|
||||
"YYYY-MM-DD HH:mm:ss A",
|
||||
uiTimezone
|
||||
);
|
||||
|
||||
return (
|
||||
<TableRow key={check._id}>
|
||||
<TableCell>{monitors[check.monitorId]?.name}</TableCell>
|
||||
<TableCell>
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{formattedDate}</TableCell>
|
||||
<TableCell>
|
||||
{check.statusCode ? check.statusCode : "N/A"}
|
||||
</TableCell>
|
||||
<TableCell>{check.message}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{paginationComponent}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<TableRow key={check._id}>
|
||||
<TableCell>{monitors[check.monitorId]?.name}</TableCell>
|
||||
<TableCell>
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{formattedDate}</TableCell>
|
||||
<TableCell>{check.statusCode ? check.statusCode : "N/A"}</TableCell>
|
||||
<TableCell>{check.message}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{paginationComponent}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
IncidentTable.propTypes = {
|
||||
monitors: PropTypes.object.isRequired,
|
||||
selectedMonitor: PropTypes.string.isRequired,
|
||||
filter: PropTypes.string.isRequired,
|
||||
monitors: PropTypes.object.isRequired,
|
||||
selectedMonitor: PropTypes.string.isRequired,
|
||||
filter: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default IncidentTable;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
.incidents h1.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
font-weight: 600;
|
||||
font-size: var(--env-var-font-size-large);
|
||||
font-weight: 600;
|
||||
}
|
||||
.incidents button.MuiButtonBase-root,
|
||||
.incidents p.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.incidents button.MuiButtonBase-root {
|
||||
height: 34px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
@@ -11,119 +11,127 @@ import SkeletonLayout from "./skeleton";
|
||||
import "./index.css";
|
||||
|
||||
const Incidents = () => {
|
||||
const theme = useTheme();
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const { monitorId } = useParams();
|
||||
const theme = useTheme();
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const { monitorId } = useParams();
|
||||
|
||||
const [monitors, setMonitors] = useState({});
|
||||
const [selectedMonitor, setSelectedMonitor] = useState("0");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [monitors, setMonitors] = useState({});
|
||||
const [selectedMonitor, setSelectedMonitor] = useState("0");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// TODO do something with these filters
|
||||
const [filter, setFilter] = useState("all");
|
||||
// TODO do something with these filters
|
||||
const [filter, setFilter] = useState("all");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMonitors = async () => {
|
||||
setLoading(true);
|
||||
const res = await networkService.getMonitorsByTeamId({
|
||||
authToken: authState.authToken,
|
||||
teamId: authState.user.teamId,
|
||||
limit: -1,
|
||||
types: null,
|
||||
status: null,
|
||||
checkOrder: null,
|
||||
normalize: null,
|
||||
page: null,
|
||||
rowsPerPage: null,
|
||||
filter: null,
|
||||
field: null,
|
||||
order: null,
|
||||
});
|
||||
// Reduce to a lookup object for 0(1) lookup
|
||||
if (res?.data?.data?.monitors?.length > 0) {
|
||||
const monitorLookup = res.data.data.monitors.reduce((acc, monitor) => {
|
||||
acc[monitor._id] = monitor;
|
||||
return acc;
|
||||
}, {});
|
||||
setMonitors(monitorLookup);
|
||||
monitorId !== undefined && setSelectedMonitor(monitorId);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
useEffect(() => {
|
||||
const fetchMonitors = async () => {
|
||||
setLoading(true);
|
||||
const res = await networkService.getMonitorsByTeamId({
|
||||
authToken: authState.authToken,
|
||||
teamId: authState.user.teamId,
|
||||
limit: -1,
|
||||
types: null,
|
||||
status: null,
|
||||
checkOrder: null,
|
||||
normalize: null,
|
||||
page: null,
|
||||
rowsPerPage: null,
|
||||
filter: null,
|
||||
field: null,
|
||||
order: null,
|
||||
});
|
||||
// Reduce to a lookup object for 0(1) lookup
|
||||
if (res?.data?.data?.monitors?.length > 0) {
|
||||
const monitorLookup = res.data.data.monitors.reduce((acc, monitor) => {
|
||||
acc[monitor._id] = monitor;
|
||||
return acc;
|
||||
}, {});
|
||||
setMonitors(monitorLookup);
|
||||
monitorId !== undefined && setSelectedMonitor(monitorId);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
fetchMonitors();
|
||||
}, [authState]);
|
||||
fetchMonitors();
|
||||
}, [authState]);
|
||||
|
||||
useEffect(() => {}, []);
|
||||
useEffect(() => {}, []);
|
||||
|
||||
const handleSelect = (event) => {
|
||||
setSelectedMonitor(event.target.value);
|
||||
};
|
||||
const handleSelect = (event) => {
|
||||
setSelectedMonitor(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack className="incidents table-container" pt={theme.spacing(6)} gap={theme.spacing(12)}>
|
||||
{loading ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
<>
|
||||
<Stack direction="row" alignItems="center" gap={theme.spacing(6)}>
|
||||
<Typography
|
||||
display="inline-block"
|
||||
component="h1"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
Incidents for
|
||||
</Typography>
|
||||
<Select
|
||||
id="incidents-select-monitor"
|
||||
placeholder="All servers"
|
||||
value={selectedMonitor}
|
||||
onChange={handleSelect}
|
||||
items={Object.values(monitors)}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.main,
|
||||
}}
|
||||
/>
|
||||
<ButtonGroup
|
||||
sx={{
|
||||
ml: "auto",
|
||||
"& .MuiButtonBase-root, & .MuiButtonBase-root:hover": {
|
||||
borderColor: theme.palette.border.light,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(filter === "all").toString()}
|
||||
onClick={() => setFilter("all")}
|
||||
>
|
||||
All
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(filter === "down").toString()}
|
||||
onClick={() => setFilter("down")}
|
||||
>
|
||||
Down
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(filter === "resolve").toString()}
|
||||
onClick={() => setFilter("resolve")}
|
||||
>
|
||||
Cannot Resolve
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
<IncidentTable
|
||||
monitors={monitors}
|
||||
selectedMonitor={selectedMonitor}
|
||||
filter={filter}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
className="incidents table-container"
|
||||
pt={theme.spacing(6)}
|
||||
gap={theme.spacing(12)}
|
||||
>
|
||||
{loading ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
<>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(6)}
|
||||
>
|
||||
<Typography
|
||||
display="inline-block"
|
||||
component="h1"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
Incidents for
|
||||
</Typography>
|
||||
<Select
|
||||
id="incidents-select-monitor"
|
||||
placeholder="All servers"
|
||||
value={selectedMonitor}
|
||||
onChange={handleSelect}
|
||||
items={Object.values(monitors)}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.main,
|
||||
}}
|
||||
/>
|
||||
<ButtonGroup
|
||||
sx={{
|
||||
ml: "auto",
|
||||
"& .MuiButtonBase-root, & .MuiButtonBase-root:hover": {
|
||||
borderColor: theme.palette.border.light,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(filter === "all").toString()}
|
||||
onClick={() => setFilter("all")}
|
||||
>
|
||||
All
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(filter === "down").toString()}
|
||||
onClick={() => setFilter("down")}
|
||||
>
|
||||
Down
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(filter === "resolve").toString()}
|
||||
onClick={() => setFilter("resolve")}
|
||||
>
|
||||
Cannot Resolve
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
<IncidentTable
|
||||
monitors={monitors}
|
||||
selectedMonitor={selectedMonitor}
|
||||
filter={filter}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Incidents;
|
||||
|
||||
@@ -7,24 +7,44 @@ import { useTheme } from "@emotion/react";
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" alignItems="center" gap={theme.spacing(6)}>
|
||||
<Skeleton variant="rounded" width={150} height={34} />
|
||||
<Skeleton variant="rounded" width="15%" height={34} />
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="20%"
|
||||
height={34}
|
||||
sx={{ ml: "auto" }}
|
||||
/>
|
||||
</Stack>
|
||||
<Skeleton variant="rounded" width="100%" height={300} />
|
||||
<Skeleton variant="rounded" width="100%" height={100} />
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(6)}
|
||||
>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width={150}
|
||||
height={34}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="15%"
|
||||
height={34}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="20%"
|
||||
height={34}
|
||||
sx={{ ml: "auto" }}
|
||||
/>
|
||||
</Stack>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={300}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={100}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
.integrations h1.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
font-weight: 600;
|
||||
font-size: var(--env-var-font-size-large);
|
||||
font-weight: 600;
|
||||
}
|
||||
.integrations p.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.integrations button {
|
||||
height: var(--env-var-height-2);
|
||||
height: var(--env-var-height-2);
|
||||
}
|
||||
|
||||
@@ -17,54 +17,61 @@ import "./index.css";
|
||||
* @returns {JSX.Element} The JSX representation of the IntegrationsComponent.
|
||||
*/
|
||||
const IntegrationsComponent = ({ icon, header, info, onClick }) => {
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Grid item lg={6} flexGrow={1}>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.spacing(12)}
|
||||
p={theme.spacing(8)}
|
||||
pl={theme.spacing(12)}
|
||||
height="100%"
|
||||
border={1}
|
||||
borderColor={theme.palette.border.light}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
backgroundColor={theme.palette.background.main}
|
||||
>
|
||||
{icon}
|
||||
<Stack gap={theme.spacing(2)} flex={1}>
|
||||
<Typography component="h1">{header}</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
maxWidth: "300px",
|
||||
wordWrap: "break-word",
|
||||
}}
|
||||
>
|
||||
{info}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={onClick}
|
||||
sx={{ alignSelf: "center" }}
|
||||
disabled={true}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</Stack>
|
||||
</Grid>
|
||||
);
|
||||
return (
|
||||
<Grid
|
||||
item
|
||||
lg={6}
|
||||
flexGrow={1}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.spacing(12)}
|
||||
p={theme.spacing(8)}
|
||||
pl={theme.spacing(12)}
|
||||
height="100%"
|
||||
border={1}
|
||||
borderColor={theme.palette.border.light}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
backgroundColor={theme.palette.background.main}
|
||||
>
|
||||
{icon}
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
flex={1}
|
||||
>
|
||||
<Typography component="h1">{header}</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
maxWidth: "300px",
|
||||
wordWrap: "break-word",
|
||||
}}
|
||||
>
|
||||
{info}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={onClick}
|
||||
sx={{ alignSelf: "center" }}
|
||||
disabled={true}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</Stack>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
// PropTypes for IntegrationsComponent
|
||||
IntegrationsComponent.propTypes = {
|
||||
icon: PropTypes.object.isRequired,
|
||||
header: PropTypes.string.isRequired,
|
||||
info: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
icon: PropTypes.object.isRequired,
|
||||
header: PropTypes.string.isRequired,
|
||||
info: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -73,73 +80,76 @@ IntegrationsComponent.propTypes = {
|
||||
*/
|
||||
|
||||
const Integrations = () => {
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
|
||||
const integrations = [
|
||||
{
|
||||
icon: (
|
||||
<Slack
|
||||
alt="slack integration"
|
||||
style={{ width: "45px", height: "45px", alignSelf: "center" }}
|
||||
/>
|
||||
),
|
||||
header: "Slack",
|
||||
info: "Connect with Slack and see incidents in a channel",
|
||||
onClick: () => {},
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<Discord
|
||||
alt="discord integration"
|
||||
style={{ width: "42px", height: "42px", alignSelf: "center" }}
|
||||
/>
|
||||
),
|
||||
header: "Discord",
|
||||
info: "Connect with Discord and view incidents directly in a channel",
|
||||
onClick: () => {},
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<Zapier
|
||||
alt="zapier integration"
|
||||
style={{ width: "42px", height: "42px", alignSelf: "center" }}
|
||||
/>
|
||||
),
|
||||
header: "Zapier",
|
||||
info: "Send all incidents to Zapier, and then see them everywhere",
|
||||
onClick: () => {},
|
||||
},
|
||||
// Add more integrations as needed
|
||||
];
|
||||
const integrations = [
|
||||
{
|
||||
icon: (
|
||||
<Slack
|
||||
alt="slack integration"
|
||||
style={{ width: "45px", height: "45px", alignSelf: "center" }}
|
||||
/>
|
||||
),
|
||||
header: "Slack",
|
||||
info: "Connect with Slack and see incidents in a channel",
|
||||
onClick: () => {},
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<Discord
|
||||
alt="discord integration"
|
||||
style={{ width: "42px", height: "42px", alignSelf: "center" }}
|
||||
/>
|
||||
),
|
||||
header: "Discord",
|
||||
info: "Connect with Discord and view incidents directly in a channel",
|
||||
onClick: () => {},
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<Zapier
|
||||
alt="zapier integration"
|
||||
style={{ width: "42px", height: "42px", alignSelf: "center" }}
|
||||
/>
|
||||
),
|
||||
header: "Zapier",
|
||||
info: "Send all incidents to Zapier, and then see them everywhere",
|
||||
onClick: () => {},
|
||||
},
|
||||
// Add more integrations as needed
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className="integrations"
|
||||
pt={theme.spacing(20)}
|
||||
gap={theme.spacing(2)}
|
||||
sx={{
|
||||
"& h1, & p": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography component="h1">Integrations</Typography>
|
||||
<Typography mb={theme.spacing(12)}>
|
||||
Connect BlueWave Uptime to your favorite service.
|
||||
</Typography>
|
||||
<Grid container spacing={theme.spacing(12)}>
|
||||
{integrations.map((integration, index) => (
|
||||
<IntegrationsComponent
|
||||
key={index}
|
||||
icon={integration.icon}
|
||||
header={integration.header}
|
||||
info={integration.info}
|
||||
onClick={integration.onClick}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
className="integrations"
|
||||
pt={theme.spacing(20)}
|
||||
gap={theme.spacing(2)}
|
||||
sx={{
|
||||
"& h1, & p": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography component="h1">Integrations</Typography>
|
||||
<Typography mb={theme.spacing(12)}>
|
||||
Connect BlueWave Uptime to your favorite service.
|
||||
</Typography>
|
||||
<Grid
|
||||
container
|
||||
spacing={theme.spacing(12)}
|
||||
>
|
||||
{integrations.map((integration, index) => (
|
||||
<IntegrationsComponent
|
||||
key={index}
|
||||
icon={integration.icon}
|
||||
header={integration.header}
|
||||
info={integration.info}
|
||||
onClick={integration.onClick}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Integrations;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
.create-maintenance button {
|
||||
height: 35px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,25 +1,25 @@
|
||||
import { Stack, styled } from "@mui/material";
|
||||
|
||||
export const ConfigBox = styled(Stack)(({ theme }) => ({
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.spacing(2),
|
||||
backgroundColor: theme.palette.background.main,
|
||||
"& > *": { padding: theme.spacing(14) },
|
||||
"& > :first-of-type, & > .MuiStack-root > div:first-of-type": {
|
||||
flex: 0.6,
|
||||
},
|
||||
"& > div:last-of-type, & > .MuiStack-root > div:last-of-type": {
|
||||
flex: 1,
|
||||
},
|
||||
"& > .MuiStack-root > div:first-of-type": { paddingRight: theme.spacing(14) },
|
||||
"& > .MuiStack-root > div:last-of-type": {
|
||||
paddingLeft: theme.spacing(14),
|
||||
},
|
||||
"& h2": { fontSize: 13.5, fontWeight: 500 },
|
||||
"& h3, & p": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
"& h3": { fontWeight: 500 },
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.spacing(2),
|
||||
backgroundColor: theme.palette.background.main,
|
||||
"& > *": { padding: theme.spacing(14) },
|
||||
"& > :first-of-type, & > .MuiStack-root > div:first-of-type": {
|
||||
flex: 0.6,
|
||||
},
|
||||
"& > div:last-of-type, & > .MuiStack-root > div:last-of-type": {
|
||||
flex: 1,
|
||||
},
|
||||
"& > .MuiStack-root > div:first-of-type": { paddingRight: theme.spacing(14) },
|
||||
"& > .MuiStack-root > div:last-of-type": {
|
||||
paddingLeft: theme.spacing(14),
|
||||
},
|
||||
"& h2": { fontSize: 13.5, fontWeight: 500 },
|
||||
"& h3, & p": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
"& h3": { fontWeight: 500 },
|
||||
}));
|
||||
|
||||
@@ -3,13 +3,13 @@ import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Modal,
|
||||
Stack,
|
||||
Typography,
|
||||
Button,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Modal,
|
||||
Stack,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { logger } from "../../../../Utils/Logger";
|
||||
@@ -19,211 +19,215 @@ import { networkService } from "../../../../main";
|
||||
import { createToast } from "../../../../Utils/toastUtils";
|
||||
|
||||
const ActionsMenu = ({ isAdmin, maintenanceWindow, updateCallback }) => {
|
||||
maintenanceWindow;
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
maintenanceWindow;
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
|
||||
const handleRemove = async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await networkService.deleteMaintenanceWindow({
|
||||
authToken,
|
||||
maintenanceWindowId: maintenanceWindow._id,
|
||||
});
|
||||
updateCallback();
|
||||
createToast({ body: "Maintenance window deleted successfully." });
|
||||
} catch (error) {
|
||||
createToast({ body: "Failed to delete maintenance window." });
|
||||
logger.error("Failed to delete maintenance window", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
setIsOpen(false);
|
||||
};
|
||||
const handleRemove = async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await networkService.deleteMaintenanceWindow({
|
||||
authToken,
|
||||
maintenanceWindowId: maintenanceWindow._id,
|
||||
});
|
||||
updateCallback();
|
||||
createToast({ body: "Maintenance window deleted successfully." });
|
||||
} catch (error) {
|
||||
createToast({ body: "Failed to delete maintenance window." });
|
||||
logger.error("Failed to delete maintenance window", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handlePause = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const data = {
|
||||
active: !maintenanceWindow.active,
|
||||
};
|
||||
await networkService.editMaintenanceWindow({
|
||||
authToken,
|
||||
maintenanceWindowId: maintenanceWindow._id,
|
||||
maintenanceWindow: data,
|
||||
});
|
||||
updateCallback();
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
createToast({ body: "Failed to pause maintenance window." });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
const handlePause = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const data = {
|
||||
active: !maintenanceWindow.active,
|
||||
};
|
||||
await networkService.editMaintenanceWindow({
|
||||
authToken,
|
||||
maintenanceWindowId: maintenanceWindow._id,
|
||||
maintenanceWindow: data,
|
||||
});
|
||||
updateCallback();
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
createToast({ body: "Failed to pause maintenance window." });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
navigate(`/maintenance/create/${maintenanceWindow._id}`);
|
||||
};
|
||||
const handleEdit = () => {
|
||||
navigate(`/maintenance/create/${maintenanceWindow._id}`);
|
||||
};
|
||||
|
||||
const openMenu = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
const openMenu = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const openRemove = (e) => {
|
||||
closeMenu(e);
|
||||
setIsOpen(true);
|
||||
};
|
||||
const openRemove = (e) => {
|
||||
closeMenu(e);
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const closeMenu = (e) => {
|
||||
e.stopPropagation();
|
||||
setAnchorEl(null);
|
||||
};
|
||||
const closeMenu = (e) => {
|
||||
e.stopPropagation();
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
aria-label="monitor actions"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
openMenu(event);
|
||||
}}
|
||||
sx={{
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"& svg path": {
|
||||
stroke: theme.palette.other.icon,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Settings />
|
||||
</IconButton>
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
aria-label="monitor actions"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
openMenu(event);
|
||||
}}
|
||||
sx={{
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"& svg path": {
|
||||
stroke: theme.palette.other.icon,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Settings />
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
className="actions-menu"
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={(e) => closeMenu(e)}
|
||||
disableScrollLock
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
"& ul": { p: theme.spacing(2.5) },
|
||||
"& li": { m: 0 },
|
||||
"& li:last-of-type": {
|
||||
color: theme.palette.error.text,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
closeMenu(e);
|
||||
e.stopPropagation();
|
||||
handleEdit();
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
handlePause();
|
||||
closeMenu(e);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{`${maintenanceWindow.active === true ? "Pause" : "Resume"}`}
|
||||
</MenuItem>
|
||||
<Menu
|
||||
className="actions-menu"
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={(e) => closeMenu(e)}
|
||||
disableScrollLock
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
"& ul": { p: theme.spacing(2.5) },
|
||||
"& li": { m: 0 },
|
||||
"& li:last-of-type": {
|
||||
color: theme.palette.error.text,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
closeMenu(e);
|
||||
e.stopPropagation();
|
||||
handleEdit();
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
handlePause();
|
||||
closeMenu(e);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{`${maintenanceWindow.active === true ? "Pause" : "Resume"}`}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openRemove(e);
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<Modal
|
||||
aria-labelledby="modal-delete-monitor"
|
||||
aria-describedby="delete-monitor-confirmation"
|
||||
open={isOpen}
|
||||
onClose={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 400,
|
||||
bgcolor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxShadow: 24,
|
||||
p: theme.spacing(15),
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography id="modal-delete-maintenance" component="h2" variant="h2">
|
||||
Do you really want to remove this maintenance window?
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
mt={theme.spacing(12)}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<LoadingButton
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(e);
|
||||
handleRemove(e);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openRemove(e);
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<Modal
|
||||
aria-labelledby="modal-delete-monitor"
|
||||
aria-describedby="delete-monitor-confirmation"
|
||||
open={isOpen}
|
||||
onClose={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 400,
|
||||
bgcolor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxShadow: 24,
|
||||
p: theme.spacing(15),
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
id="modal-delete-maintenance"
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
Do you really want to remove this maintenance window?
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
mt={theme.spacing(12)}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<LoadingButton
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(e);
|
||||
handleRemove(e);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ActionsMenu.propTypes = {
|
||||
maintenanceWindow: PropTypes.object,
|
||||
isAdmin: PropTypes.bool,
|
||||
updateCallback: PropTypes.func,
|
||||
maintenanceWindow: PropTypes.object,
|
||||
isAdmin: PropTypes.bool,
|
||||
updateCallback: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ActionsMenu;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
TableContainer,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
Paper,
|
||||
Box,
|
||||
TablePagination,
|
||||
Stack,
|
||||
Typography,
|
||||
Button,
|
||||
TableContainer,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
Paper,
|
||||
Box,
|
||||
TablePagination,
|
||||
Stack,
|
||||
Typography,
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
|
||||
import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
|
||||
@@ -42,321 +42,321 @@ import dayjs from "dayjs";
|
||||
* @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));
|
||||
};
|
||||
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>
|
||||
);
|
||||
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,
|
||||
count: PropTypes.number.isRequired,
|
||||
page: PropTypes.number.isRequired,
|
||||
rowsPerPage: PropTypes.number.isRequired,
|
||||
onPageChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const MaintenanceTable = ({
|
||||
page,
|
||||
setPage,
|
||||
sort,
|
||||
setSort,
|
||||
maintenanceWindows,
|
||||
maintenanceWindowCount,
|
||||
updateCallback,
|
||||
page,
|
||||
setPage,
|
||||
sort,
|
||||
setSort,
|
||||
maintenanceWindows,
|
||||
maintenanceWindowCount,
|
||||
updateCallback,
|
||||
}) => {
|
||||
const { rowsPerPage } = useSelector((state) => state.ui.maintenance);
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const { rowsPerPage } = useSelector((state) => state.ui.maintenance);
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const getTimeToNextWindow = (startTime, endTime, repeat) => {
|
||||
//1. Advance time closest to next window as possible
|
||||
const now = dayjs();
|
||||
let start = dayjs(startTime);
|
||||
let end = dayjs(endTime);
|
||||
if (repeat > 0) {
|
||||
// Advance time closest to next window as possible
|
||||
while (start.isBefore(now) && end.isBefore(now)) {
|
||||
start = start.add(repeat, "milliseconds");
|
||||
end = end.add(repeat, "milliseconds");
|
||||
}
|
||||
}
|
||||
const getTimeToNextWindow = (startTime, endTime, repeat) => {
|
||||
//1. Advance time closest to next window as possible
|
||||
const now = dayjs();
|
||||
let start = dayjs(startTime);
|
||||
let end = dayjs(endTime);
|
||||
if (repeat > 0) {
|
||||
// Advance time closest to next window as possible
|
||||
while (start.isBefore(now) && end.isBefore(now)) {
|
||||
start = start.add(repeat, "milliseconds");
|
||||
end = end.add(repeat, "milliseconds");
|
||||
}
|
||||
}
|
||||
|
||||
//Check if we are in a window
|
||||
if (now.isAfter(start) && now.isBefore(end)) {
|
||||
return "In maintenance window";
|
||||
}
|
||||
//Check if we are in a window
|
||||
if (now.isAfter(start) && now.isBefore(end)) {
|
||||
return "In maintenance window";
|
||||
}
|
||||
|
||||
if (start.isAfter(now)) {
|
||||
const diffInMinutes = start.diff(now, "minutes");
|
||||
const diffInHours = start.diff(now, "hours");
|
||||
const diffInDays = start.diff(now, "days");
|
||||
if (start.isAfter(now)) {
|
||||
const diffInMinutes = start.diff(now, "minutes");
|
||||
const diffInHours = start.diff(now, "hours");
|
||||
const diffInDays = start.diff(now, "days");
|
||||
|
||||
if (diffInMinutes < 60) {
|
||||
return diffInMinutes + " minutes";
|
||||
} else if (diffInHours < 24) {
|
||||
return diffInHours + " hours";
|
||||
} else if (diffInDays < 7) {
|
||||
return diffInDays + " days";
|
||||
} else {
|
||||
return diffInDays + " days";
|
||||
}
|
||||
}
|
||||
};
|
||||
if (diffInMinutes < 60) {
|
||||
return diffInMinutes + " minutes";
|
||||
} else if (diffInHours < 24) {
|
||||
return diffInHours + " hours";
|
||||
} else if (diffInDays < 7) {
|
||||
return diffInDays + " days";
|
||||
} else {
|
||||
return diffInDays + " days";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
dispatch(
|
||||
setRowsPerPage({
|
||||
value: parseInt(event.target.value, 10),
|
||||
table: "maintenance",
|
||||
})
|
||||
);
|
||||
setPage(0);
|
||||
};
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
dispatch(
|
||||
setRowsPerPage({
|
||||
value: parseInt(event.target.value, 10),
|
||||
table: "maintenance",
|
||||
})
|
||||
);
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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,
|
||||
maintenanceWindowCount
|
||||
);
|
||||
return `${start} - ${end}`;
|
||||
};
|
||||
/**
|
||||
* 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, maintenanceWindowCount);
|
||||
return `${start} - ${end}`;
|
||||
};
|
||||
|
||||
const handleSort = async (field) => {
|
||||
let order = "";
|
||||
if (sort.field !== field) {
|
||||
order = "desc";
|
||||
} else {
|
||||
order = sort.order === "asc" ? "desc" : "asc";
|
||||
}
|
||||
setSort({ field, order });
|
||||
};
|
||||
const handleSort = async (field) => {
|
||||
let order = "";
|
||||
if (sort.field !== field) {
|
||||
order = "desc";
|
||||
} else {
|
||||
order = sort.order === "asc" ? "desc" : "asc";
|
||||
}
|
||||
setSort({ field, order });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("name")}
|
||||
>
|
||||
<Box>
|
||||
Maintenance Window Name
|
||||
<span
|
||||
style={{
|
||||
visibility: sort.field === "name" ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{sort.order === "asc" ? (
|
||||
<ArrowUpwardRoundedIcon />
|
||||
) : (
|
||||
<ArrowDownwardRoundedIcon />
|
||||
)}
|
||||
</span>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("status")}
|
||||
>
|
||||
{" "}
|
||||
<Box width="max-content">
|
||||
{" "}
|
||||
Status
|
||||
<span
|
||||
style={{
|
||||
visibility:
|
||||
sort.field === "active" ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{sort.order === "asc" ? (
|
||||
<ArrowUpwardRoundedIcon />
|
||||
) : (
|
||||
<ArrowDownwardRoundedIcon />
|
||||
)}
|
||||
</span>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>Next Window</TableCell>
|
||||
<TableCell>Repeat</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{maintenanceWindows.map((maintenanceWindow) => {
|
||||
const text = maintenanceWindow.active ? "active" : "paused";
|
||||
const status = maintenanceWindow.active ? "up" : "paused";
|
||||
return (
|
||||
<TableRow key={maintenanceWindow._id}>
|
||||
<TableCell>{maintenanceWindow.name}</TableCell>
|
||||
<TableCell>
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={text}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{getTimeToNextWindow(
|
||||
maintenanceWindow.start,
|
||||
maintenanceWindow.end,
|
||||
maintenanceWindow.repeat
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{maintenanceWindow.repeat === 0
|
||||
? "N/A"
|
||||
: formatDurationRounded(maintenanceWindow.repeat)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ActionsMenu
|
||||
maintenanceWindow={maintenanceWindow}
|
||||
updateCallback={updateCallback}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</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 {maintenanceWindowCount} maintenance window(s)
|
||||
</Typography>
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={maintenanceWindowCount}
|
||||
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>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("name")}
|
||||
>
|
||||
<Box>
|
||||
Maintenance Window Name
|
||||
<span
|
||||
style={{
|
||||
visibility: sort.field === "name" ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{sort.order === "asc" ? (
|
||||
<ArrowUpwardRoundedIcon />
|
||||
) : (
|
||||
<ArrowDownwardRoundedIcon />
|
||||
)}
|
||||
</span>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("status")}
|
||||
>
|
||||
{" "}
|
||||
<Box width="max-content">
|
||||
{" "}
|
||||
Status
|
||||
<span
|
||||
style={{
|
||||
visibility: sort.field === "active" ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{sort.order === "asc" ? (
|
||||
<ArrowUpwardRoundedIcon />
|
||||
) : (
|
||||
<ArrowDownwardRoundedIcon />
|
||||
)}
|
||||
</span>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>Next Window</TableCell>
|
||||
<TableCell>Repeat</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{maintenanceWindows.map((maintenanceWindow) => {
|
||||
const text = maintenanceWindow.active ? "active" : "paused";
|
||||
const status = maintenanceWindow.active ? "up" : "paused";
|
||||
return (
|
||||
<TableRow key={maintenanceWindow._id}>
|
||||
<TableCell>{maintenanceWindow.name}</TableCell>
|
||||
<TableCell>
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={text}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{getTimeToNextWindow(
|
||||
maintenanceWindow.start,
|
||||
maintenanceWindow.end,
|
||||
maintenanceWindow.repeat
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{maintenanceWindow.repeat === 0
|
||||
? "N/A"
|
||||
: formatDurationRounded(maintenanceWindow.repeat)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ActionsMenu
|
||||
maintenanceWindow={maintenanceWindow}
|
||||
updateCallback={updateCallback}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</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 {maintenanceWindowCount} maintenance window(s)
|
||||
</Typography>
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={maintenanceWindowCount}
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MaintenanceTable.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
page: PropTypes.number,
|
||||
setPage: PropTypes.func,
|
||||
rowsPerPage: PropTypes.number,
|
||||
setRowsPerPage: PropTypes.func,
|
||||
sort: PropTypes.object,
|
||||
setSort: PropTypes.func,
|
||||
maintenanceWindows: PropTypes.array,
|
||||
maintenanceWindowCount: PropTypes.number,
|
||||
updateCallback: PropTypes.func,
|
||||
isAdmin: PropTypes.bool,
|
||||
page: PropTypes.number,
|
||||
setPage: PropTypes.func,
|
||||
rowsPerPage: PropTypes.number,
|
||||
setRowsPerPage: PropTypes.func,
|
||||
sort: PropTypes.object,
|
||||
setSort: PropTypes.func,
|
||||
maintenanceWindows: PropTypes.array,
|
||||
maintenanceWindowCount: PropTypes.number,
|
||||
updateCallback: PropTypes.func,
|
||||
};
|
||||
|
||||
const MemoizedMaintenanceTable = memo(MaintenanceTable);
|
||||
|
||||
@@ -10,103 +10,100 @@ import Breadcrumbs from "../../Components/Breadcrumbs";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const Maintenance = ({ isAdmin }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const { rowsPerPage } = useSelector((state) => state.ui.maintenance);
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const { rowsPerPage } = useSelector((state) => state.ui.maintenance);
|
||||
|
||||
const [maintenanceWindows, setMaintenanceWindows] = useState([]);
|
||||
const [maintenanceWindowCount, setMaintenanceWindowCount] = useState(0);
|
||||
const [page, setPage] = useState(0);
|
||||
const [sort, setSort] = useState({});
|
||||
const [updateTrigger, setUpdateTrigger] = useState(false);
|
||||
const [maintenanceWindows, setMaintenanceWindows] = useState([]);
|
||||
const [maintenanceWindowCount, setMaintenanceWindowCount] = useState(0);
|
||||
const [page, setPage] = useState(0);
|
||||
const [sort, setSort] = useState({});
|
||||
const [updateTrigger, setUpdateTrigger] = useState(false);
|
||||
|
||||
const handleActionMenuDelete = () => {
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
};
|
||||
const handleActionMenuDelete = () => {
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMaintenanceWindows = async () => {
|
||||
try {
|
||||
const response = await networkService.getMaintenanceWindowsByTeamId({
|
||||
authToken: authToken,
|
||||
page: page,
|
||||
rowsPerPage: rowsPerPage,
|
||||
});
|
||||
const { maintenanceWindows, maintenanceWindowCount } =
|
||||
response.data.data;
|
||||
setMaintenanceWindows(maintenanceWindows);
|
||||
setMaintenanceWindowCount(maintenanceWindowCount);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
fetchMaintenanceWindows();
|
||||
}, [authToken, page, rowsPerPage, updateTrigger]);
|
||||
useEffect(() => {
|
||||
const fetchMaintenanceWindows = async () => {
|
||||
try {
|
||||
const response = await networkService.getMaintenanceWindowsByTeamId({
|
||||
authToken: authToken,
|
||||
page: page,
|
||||
rowsPerPage: rowsPerPage,
|
||||
});
|
||||
const { maintenanceWindows, maintenanceWindowCount } = response.data.data;
|
||||
setMaintenanceWindows(maintenanceWindows);
|
||||
setMaintenanceWindowCount(maintenanceWindowCount);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
fetchMaintenanceWindows();
|
||||
}, [authToken, page, rowsPerPage, updateTrigger]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="maintenance table-container"
|
||||
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",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{maintenanceWindows.length > 0 && (
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
mt={theme.spacing(5)}
|
||||
>
|
||||
<Breadcrumbs
|
||||
list={[{ name: "maintenance", path: "/maintenance" }]}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
navigate("/maintenance/create");
|
||||
}}
|
||||
sx={{ fontWeight: 500 }}
|
||||
>
|
||||
Create maintenance window
|
||||
</Button>
|
||||
</Stack>
|
||||
<MaintenanceTable
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
sort={sort}
|
||||
setSort={setSort}
|
||||
maintenanceWindows={maintenanceWindows}
|
||||
maintenanceWindowCount={maintenanceWindowCount}
|
||||
updateCallback={handleActionMenuDelete}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{maintenanceWindows.length === 0 && (
|
||||
<Fallback
|
||||
title="maintenance window"
|
||||
checks={[
|
||||
"Mark your maintenance periods",
|
||||
"Eliminate any misunderstandings",
|
||||
"Stop sending alerts in maintenance windows",
|
||||
]}
|
||||
link="/maintenance/create"
|
||||
isAdmin={true}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
return (
|
||||
<Box
|
||||
className="maintenance table-container"
|
||||
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",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{maintenanceWindows.length > 0 && (
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
mt={theme.spacing(5)}
|
||||
>
|
||||
<Breadcrumbs list={[{ name: "maintenance", path: "/maintenance" }]} />
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
navigate("/maintenance/create");
|
||||
}}
|
||||
sx={{ fontWeight: 500 }}
|
||||
>
|
||||
Create maintenance window
|
||||
</Button>
|
||||
</Stack>
|
||||
<MaintenanceTable
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
sort={sort}
|
||||
setSort={setSort}
|
||||
maintenanceWindows={maintenanceWindows}
|
||||
maintenanceWindowCount={maintenanceWindowCount}
|
||||
updateCallback={handleActionMenuDelete}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{maintenanceWindows.length === 0 && (
|
||||
<Fallback
|
||||
title="maintenance window"
|
||||
checks={[
|
||||
"Mark your maintenance periods",
|
||||
"Eliminate any misunderstandings",
|
||||
"Stop sending alerts in maintenance windows",
|
||||
]}
|
||||
link="/maintenance/create"
|
||||
isAdmin={true}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Maintenance;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
.configure-monitor button.MuiButtonBase-root {
|
||||
height: var(--env-var-height-2);
|
||||
height: var(--env-var-height-2);
|
||||
}
|
||||
|
||||
.configure-monitor .MuiStack-root:has(span.MuiTypography-root.input-error) {
|
||||
position: relative;
|
||||
position: relative;
|
||||
}
|
||||
.configure-monitor span.MuiTypography-root.input-error {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ import { createToast } from "../../../Utils/toastUtils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { ConfigBox } from "../styled";
|
||||
import {
|
||||
updateUptimeMonitor,
|
||||
pauseUptimeMonitor,
|
||||
getUptimeMonitorById,
|
||||
getUptimeMonitorsByTeamId,
|
||||
deleteUptimeMonitor,
|
||||
updateUptimeMonitor,
|
||||
pauseUptimeMonitor,
|
||||
getUptimeMonitorById,
|
||||
getUptimeMonitorsByTeamId,
|
||||
deleteUptimeMonitor,
|
||||
} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import PauseIcon from "../../../assets/icons/pause-icon.svg?react";
|
||||
@@ -33,11 +33,11 @@ import Dialog from "../../../Components/Dialog";
|
||||
* @returns {URL} - The parsed URL object if valid, otherwise an empty string.
|
||||
*/
|
||||
const parseUrl = (url) => {
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -45,426 +45,430 @@ const parseUrl = (url) => {
|
||||
* @component
|
||||
*/
|
||||
const Configure = () => {
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
const { isLoading } = useSelector((state) => state.uptimeMonitors);
|
||||
const [monitor, setMonitor] = useState({});
|
||||
const [errors, setErrors] = useState({});
|
||||
const { monitorId } = useParams();
|
||||
const idMap = {
|
||||
"monitor-url": "url",
|
||||
"monitor-name": "name",
|
||||
"monitor-checks-http": "type",
|
||||
"monitor-checks-ping": "type",
|
||||
"notify-email-default": "notification-email",
|
||||
};
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
const { isLoading } = useSelector((state) => state.uptimeMonitors);
|
||||
const [monitor, setMonitor] = useState({});
|
||||
const [errors, setErrors] = useState({});
|
||||
const { monitorId } = useParams();
|
||||
const idMap = {
|
||||
"monitor-url": "url",
|
||||
"monitor-name": "name",
|
||||
"monitor-checks-http": "type",
|
||||
"monitor-checks-ping": "type",
|
||||
"notify-email-default": "notification-email",
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMonitor = async () => {
|
||||
try {
|
||||
const action = await dispatch(
|
||||
getUptimeMonitorById({ authToken, monitorId })
|
||||
);
|
||||
useEffect(() => {
|
||||
const fetchMonitor = async () => {
|
||||
try {
|
||||
const action = await dispatch(getUptimeMonitorById({ authToken, monitorId }));
|
||||
|
||||
if (getUptimeMonitorById.fulfilled.match(action)) {
|
||||
const monitor = action.payload.data;
|
||||
setMonitor(monitor);
|
||||
} else if (getUptimeMonitorById.rejected.match(action)) {
|
||||
throw new Error(action.error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error fetching monitor of id: " + monitorId);
|
||||
navigate("/not-found", { replace: true });
|
||||
}
|
||||
};
|
||||
fetchMonitor();
|
||||
}, [monitorId, authToken, navigate]);
|
||||
if (getUptimeMonitorById.fulfilled.match(action)) {
|
||||
const monitor = action.payload.data;
|
||||
setMonitor(monitor);
|
||||
} else if (getUptimeMonitorById.rejected.match(action)) {
|
||||
throw new Error(action.error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error fetching monitor of id: " + monitorId);
|
||||
navigate("/not-found", { replace: true });
|
||||
}
|
||||
};
|
||||
fetchMonitor();
|
||||
}, [monitorId, authToken, navigate]);
|
||||
|
||||
const handleChange = (event, name) => {
|
||||
let { value, id } = event.target;
|
||||
if (!name) name = idMap[id];
|
||||
const handleChange = (event, name) => {
|
||||
let { 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 {
|
||||
if (name === "interval") {
|
||||
value = value * MS_PER_MINUTE;
|
||||
}
|
||||
setMonitor((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
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 {
|
||||
if (name === "interval") {
|
||||
value = value * MS_PER_MINUTE;
|
||||
}
|
||||
setMonitor((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
|
||||
const validation = monitorValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
const validation = monitorValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
|
||||
if (validation.error)
|
||||
updatedErrors[name] = validation.error.details[0].message;
|
||||
else delete updatedErrors[name];
|
||||
return updatedErrors;
|
||||
});
|
||||
}
|
||||
};
|
||||
if (validation.error) updatedErrors[name] = validation.error.details[0].message;
|
||||
else delete updatedErrors[name];
|
||||
return updatedErrors;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handlePause = async () => {
|
||||
try {
|
||||
const action = await dispatch(
|
||||
pauseUptimeMonitor({ authToken, monitorId })
|
||||
);
|
||||
if (pauseUptimeMonitor.fulfilled.match(action)) {
|
||||
const monitor = action.payload.data;
|
||||
setMonitor(monitor);
|
||||
} else if (pauseUptimeMonitor.rejected.match(action)) {
|
||||
throw new Error(action.error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error pausing monitor: " + monitorId);
|
||||
createToast({ body: "Failed to pause monitor" });
|
||||
}
|
||||
};
|
||||
const handlePause = async () => {
|
||||
try {
|
||||
const action = await dispatch(pauseUptimeMonitor({ authToken, monitorId }));
|
||||
if (pauseUptimeMonitor.fulfilled.match(action)) {
|
||||
const monitor = action.payload.data;
|
||||
setMonitor(monitor);
|
||||
} else if (pauseUptimeMonitor.rejected.match(action)) {
|
||||
throw new Error(action.error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error pausing monitor: " + monitorId);
|
||||
createToast({ body: "Failed to pause monitor" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
const action = await dispatch(
|
||||
updateUptimeMonitor({ authToken, monitor: monitor })
|
||||
);
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
createToast({ body: "Monitor updated successfully!" });
|
||||
dispatch(getUptimeMonitorsByTeamId(authToken));
|
||||
} else {
|
||||
createToast({ body: "Failed to update monitor." });
|
||||
}
|
||||
};
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
const action = await dispatch(updateUptimeMonitor({ authToken, monitor: monitor }));
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
createToast({ body: "Monitor updated successfully!" });
|
||||
dispatch(getUptimeMonitorsByTeamId(authToken));
|
||||
} else {
|
||||
createToast({ body: "Failed to update monitor." });
|
||||
}
|
||||
};
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const handleRemove = async (event) => {
|
||||
event.preventDefault();
|
||||
const action = await dispatch(deleteUptimeMonitor({ authToken, monitor }));
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
navigate("/monitors");
|
||||
} else {
|
||||
createToast({ body: "Failed to delete monitor." });
|
||||
}
|
||||
};
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const handleRemove = async (event) => {
|
||||
event.preventDefault();
|
||||
const action = await dispatch(deleteUptimeMonitor({ authToken, monitor }));
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
navigate("/monitors");
|
||||
} else {
|
||||
createToast({ body: "Failed to delete monitor." });
|
||||
}
|
||||
};
|
||||
|
||||
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" },
|
||||
];
|
||||
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" },
|
||||
];
|
||||
|
||||
// Parse the URL
|
||||
const parsedUrl = parseUrl(monitor?.url);
|
||||
const protocol = parsedUrl?.protocol?.replace(":", "") || "";
|
||||
// Parse the URL
|
||||
const parsedUrl = parseUrl(monitor?.url);
|
||||
const protocol = parsedUrl?.protocol?.replace(":", "") || "";
|
||||
|
||||
const statusColor = {
|
||||
true: theme.palette.success.main,
|
||||
false: theme.palette.error.main,
|
||||
undefined: theme.palette.warning.main,
|
||||
};
|
||||
const statusColor = {
|
||||
true: theme.palette.success.main,
|
||||
false: theme.palette.error.main,
|
||||
undefined: theme.palette.warning.main,
|
||||
};
|
||||
|
||||
const statusMsg = {
|
||||
true: "Your site is up.",
|
||||
false: "Your site is down.",
|
||||
undefined: "Pending...",
|
||||
};
|
||||
const statusMsg = {
|
||||
true: "Your site is up.",
|
||||
false: "Your site is down.",
|
||||
undefined: "Pending...",
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack className="configure-monitor" gap={theme.spacing(10)}>
|
||||
{Object.keys(monitor).length === 0 ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
<>
|
||||
<Breadcrumbs
|
||||
list={[
|
||||
{ name: "monitors", path: "/monitors" },
|
||||
{ name: "details", path: `/monitors/${monitorId}` },
|
||||
{ name: "configure", path: `/monitors/configure/${monitorId}` },
|
||||
]}
|
||||
/>
|
||||
<Stack
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={theme.spacing(12)}
|
||||
flex={1}
|
||||
>
|
||||
<Stack direction="row" gap={theme.spacing(2)}>
|
||||
<Box>
|
||||
<Typography component="h1" variant="h1">
|
||||
{monitor.name}
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
height="fit-content"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Tooltip
|
||||
title={statusMsg[monitor?.status ?? undefined]}
|
||||
disableInteractive
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -8],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<PulseDot
|
||||
color={statusColor[monitor?.status ?? undefined]}
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Typography component="h2" variant="h2">
|
||||
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
|
||||
</Typography>
|
||||
<Typography
|
||||
position="relative"
|
||||
variant="body2"
|
||||
ml={theme.spacing(6)}
|
||||
mt={theme.spacing(1)}
|
||||
sx={{
|
||||
"&:before": {
|
||||
position: "absolute",
|
||||
content: `""`,
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: theme.palette.text.tertiary,
|
||||
opacity: 0.8,
|
||||
left: -10,
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
Editting...
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
alignSelf: "flex-end",
|
||||
ml: "auto",
|
||||
}}
|
||||
>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
loading={isLoading}
|
||||
sx={{
|
||||
pl: theme.spacing(4),
|
||||
pr: theme.spacing(6),
|
||||
mr: theme.spacing(6),
|
||||
"& svg": {
|
||||
mr: theme.spacing(2),
|
||||
width: 22,
|
||||
height: 22,
|
||||
"& path": {
|
||||
stroke: theme.palette.text.tertiary,
|
||||
strokeWidth: 1.7,
|
||||
},
|
||||
},
|
||||
}}
|
||||
onClick={handlePause}
|
||||
>
|
||||
{monitor?.isActive ? (
|
||||
<>
|
||||
<PauseIcon />
|
||||
Pause
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ResumeIcon />
|
||||
Resume
|
||||
</>
|
||||
)}
|
||||
</LoadingButton>
|
||||
<LoadingButton
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="error"
|
||||
sx={{ px: theme.spacing(8) }}
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
Remove
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
<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(20)}>
|
||||
<Field
|
||||
type={monitor?.type === "http" ? "url" : "text"}
|
||||
https={protocol === "https"}
|
||||
id="monitor-url"
|
||||
label="URL to monitor"
|
||||
placeholder="google.com"
|
||||
value={parsedUrl?.host || monitor?.url || ""}
|
||||
disabled={true}
|
||||
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">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"
|
||||
) || false
|
||||
}
|
||||
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(20)}>
|
||||
<Select
|
||||
id="monitor-interval-configure"
|
||||
label="Check frequency"
|
||||
value={monitor?.interval / MS_PER_MINUTE || 1}
|
||||
onChange={(event) => handleChange(event, "interval")}
|
||||
items={frequencies}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Stack direction="row" justifyContent="flex-end" mt="auto">
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
loading={isLoading}
|
||||
sx={{ px: theme.spacing(12) }}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
<Dialog
|
||||
modelTitle="modal-delete-monitor"
|
||||
modelDescription="delete-monitor-confirmation"
|
||||
open={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
title="Do you really want to delete this monitor?"
|
||||
confirmationBtnLbl="Delete"
|
||||
confirmationBtnOnClick={handleRemove}
|
||||
cancelBtnLbl="Cancel"
|
||||
cancelBtnOnClick={() => setIsOpen(false)}
|
||||
theme={theme}
|
||||
isLoading={isLoading}
|
||||
description="Once deleted, this monitor cannot be retrieved."
|
||||
>
|
||||
</Dialog>
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
className="configure-monitor"
|
||||
gap={theme.spacing(10)}
|
||||
>
|
||||
{Object.keys(monitor).length === 0 ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
<>
|
||||
<Breadcrumbs
|
||||
list={[
|
||||
{ name: "monitors", path: "/monitors" },
|
||||
{ name: "details", path: `/monitors/${monitorId}` },
|
||||
{ name: "configure", path: `/monitors/configure/${monitorId}` },
|
||||
]}
|
||||
/>
|
||||
<Stack
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={theme.spacing(12)}
|
||||
flex={1}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Box>
|
||||
<Typography
|
||||
component="h1"
|
||||
variant="h1"
|
||||
>
|
||||
{monitor.name}
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
height="fit-content"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Tooltip
|
||||
title={statusMsg[monitor?.status ?? undefined]}
|
||||
disableInteractive
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -8],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<PulseDot color={statusColor[monitor?.status ?? undefined]} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
|
||||
</Typography>
|
||||
<Typography
|
||||
position="relative"
|
||||
variant="body2"
|
||||
ml={theme.spacing(6)}
|
||||
mt={theme.spacing(1)}
|
||||
sx={{
|
||||
"&:before": {
|
||||
position: "absolute",
|
||||
content: `""`,
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: theme.palette.text.tertiary,
|
||||
opacity: 0.8,
|
||||
left: -10,
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
Editting...
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
alignSelf: "flex-end",
|
||||
ml: "auto",
|
||||
}}
|
||||
>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
loading={isLoading}
|
||||
sx={{
|
||||
pl: theme.spacing(4),
|
||||
pr: theme.spacing(6),
|
||||
mr: theme.spacing(6),
|
||||
"& svg": {
|
||||
mr: theme.spacing(2),
|
||||
width: 22,
|
||||
height: 22,
|
||||
"& path": {
|
||||
stroke: theme.palette.text.tertiary,
|
||||
strokeWidth: 1.7,
|
||||
},
|
||||
},
|
||||
}}
|
||||
onClick={handlePause}
|
||||
>
|
||||
{monitor?.isActive ? (
|
||||
<>
|
||||
<PauseIcon />
|
||||
Pause
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ResumeIcon />
|
||||
Resume
|
||||
</>
|
||||
)}
|
||||
</LoadingButton>
|
||||
<LoadingButton
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="error"
|
||||
sx={{ px: theme.spacing(8) }}
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
Remove
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
<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(20)}>
|
||||
<Field
|
||||
type={monitor?.type === "http" ? "url" : "text"}
|
||||
https={protocol === "https"}
|
||||
id="monitor-url"
|
||||
label="URL to monitor"
|
||||
placeholder="google.com"
|
||||
value={parsedUrl?.host || monitor?.url || ""}
|
||||
disabled={true}
|
||||
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">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"
|
||||
) || false
|
||||
}
|
||||
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(20)}>
|
||||
<Select
|
||||
id="monitor-interval-configure"
|
||||
label="Check frequency"
|
||||
value={monitor?.interval / MS_PER_MINUTE || 1}
|
||||
onChange={(event) => handleChange(event, "interval")}
|
||||
items={frequencies}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
mt="auto"
|
||||
>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
loading={isLoading}
|
||||
sx={{ px: theme.spacing(12) }}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
<Dialog
|
||||
modelTitle="modal-delete-monitor"
|
||||
modelDescription="delete-monitor-confirmation"
|
||||
open={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
title="Do you really want to delete this monitor?"
|
||||
confirmationBtnLbl="Delete"
|
||||
confirmationBtnOnClick={handleRemove}
|
||||
cancelBtnLbl="Cancel"
|
||||
cancelBtnOnClick={() => setIsOpen(false)}
|
||||
theme={theme}
|
||||
isLoading={isLoading}
|
||||
description="Once deleted, this monitor cannot be retrieved."
|
||||
></Dialog>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Configure;
|
||||
|
||||
@@ -7,46 +7,84 @@ import { useTheme } from "@emotion/react";
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Skeleton variant="rounded" width="15%" height={34} />
|
||||
<Stack gap={theme.spacing(20)} mt={theme.spacing(6)}>
|
||||
<Stack direction="row" gap={theme.spacing(4)} mt={theme.spacing(4)}>
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
style={{ minWidth: 24, minHeight: 24 }}
|
||||
/>
|
||||
<Box width="80%">
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="50%"
|
||||
height={24}
|
||||
sx={{ mb: theme.spacing(4) }}
|
||||
/>
|
||||
<Skeleton variant="rounded" width="50%" height={18} />
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(6)}
|
||||
sx={{
|
||||
ml: "auto",
|
||||
alignSelf: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Skeleton variant="rounded" width={150} height={34} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Skeleton variant="rounded" width="100%" height={200} />
|
||||
<Skeleton variant="rounded" width="100%" height={200} />
|
||||
<Skeleton variant="rounded" width="100%" height={200} />
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Skeleton variant="rounded" width="15%" height={34} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="15%"
|
||||
height={34}
|
||||
/>
|
||||
<Stack
|
||||
gap={theme.spacing(20)}
|
||||
mt={theme.spacing(6)}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
mt={theme.spacing(4)}
|
||||
>
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
style={{ minWidth: 24, minHeight: 24 }}
|
||||
/>
|
||||
<Box width="80%">
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="50%"
|
||||
height={24}
|
||||
sx={{ mb: theme.spacing(4) }}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="50%"
|
||||
height={18}
|
||||
/>
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(6)}
|
||||
sx={{
|
||||
ml: "auto",
|
||||
alignSelf: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width={150}
|
||||
height={34}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={200}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={200}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={200}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="15%"
|
||||
height={34}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
|
||||
@@ -26,130 +26,128 @@ const CreateMonitor = () => {
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
|
||||
const idMap = {
|
||||
"monitor-url": "url",
|
||||
"monitor-name": "name",
|
||||
"monitor-checks-http": "type",
|
||||
"monitor-checks-ping": "type",
|
||||
"notify-email-default": "notification-email",
|
||||
};
|
||||
const idMap = {
|
||||
"monitor-url": "url",
|
||||
"monitor-name": "name",
|
||||
"monitor-checks-http": "type",
|
||||
"monitor-checks-ping": "type",
|
||||
"notify-email-default": "notification-email",
|
||||
};
|
||||
|
||||
const { monitorId } = useParams();
|
||||
const [monitor, setMonitor] = useState({
|
||||
url: "",
|
||||
name: "",
|
||||
type: "http",
|
||||
notifications: [],
|
||||
interval: 1,
|
||||
});
|
||||
const [https, setHttps] = useState(true);
|
||||
const [errors, setErrors] = useState({});
|
||||
const { monitorId } = useParams();
|
||||
const [monitor, setMonitor] = useState({
|
||||
url: "",
|
||||
name: "",
|
||||
type: "http",
|
||||
notifications: [],
|
||||
interval: 1,
|
||||
});
|
||||
const [https, setHttps] = useState(true);
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMonitor = async () => {
|
||||
if (monitorId) {
|
||||
const action = await dispatch(
|
||||
getUptimeMonitorById({ authToken, monitorId })
|
||||
);
|
||||
useEffect(() => {
|
||||
const fetchMonitor = async () => {
|
||||
if (monitorId) {
|
||||
const action = await dispatch(getUptimeMonitorById({ authToken, monitorId }));
|
||||
|
||||
if (action.payload.success) {
|
||||
const data = action.payload.data;
|
||||
const { name, ...rest } = data; //data.name is read-only
|
||||
if (rest.type === "http") {
|
||||
const url = new URL(rest.url);
|
||||
rest.url = url.host;
|
||||
}
|
||||
rest.name = `${name} (Clone)`;
|
||||
rest.interval /= MS_PER_MINUTE;
|
||||
setMonitor({
|
||||
...rest,
|
||||
});
|
||||
} else {
|
||||
navigate("/not-found", { replace: true });
|
||||
createToast({
|
||||
body: "There was an error cloning the monitor.",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
fetchMonitor();
|
||||
}, [monitorId, authToken, monitors]);
|
||||
if (action.payload.success) {
|
||||
const data = action.payload.data;
|
||||
const { name, ...rest } = data; //data.name is read-only
|
||||
if (rest.type === "http") {
|
||||
const url = new URL(rest.url);
|
||||
rest.url = url.host;
|
||||
}
|
||||
rest.name = `${name} (Clone)`;
|
||||
rest.interval /= MS_PER_MINUTE;
|
||||
setMonitor({
|
||||
...rest,
|
||||
});
|
||||
} else {
|
||||
navigate("/not-found", { replace: true });
|
||||
createToast({
|
||||
body: "There was an error cloning the monitor.",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
fetchMonitor();
|
||||
}, [monitorId, authToken, monitors]);
|
||||
|
||||
const handleChange = (event, name) => {
|
||||
const { value, id } = event.target;
|
||||
if (!name) name = idMap[id];
|
||||
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,
|
||||
}));
|
||||
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 { 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 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,
|
||||
});
|
||||
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 (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 })
|
||||
@@ -161,228 +159,234 @@ const CreateMonitor = () => {
|
||||
}
|
||||
}
|
||||
|
||||
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." });
|
||||
}
|
||||
}
|
||||
};
|
||||
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" },
|
||||
];
|
||||
//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" },
|
||||
];
|
||||
|
||||
return (
|
||||
<Box className="create-monitor">
|
||||
<Breadcrumbs
|
||||
list={[
|
||||
{ name: "monitors", path: "/monitors" },
|
||||
{ name: "create", path: `/monitors/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"
|
||||
variant="h2"
|
||||
fontSize="inherit"
|
||||
fontWeight="inherit"
|
||||
>
|
||||
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={monitor.type === "http" ? "url" : "text"}
|
||||
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 === "http"}
|
||||
onChange={(event) => handleChange(event)}
|
||||
/>
|
||||
{monitor.type === "http" ? (
|
||||
<ButtonGroup sx={{ ml: theme.spacing(16) }}>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={https.toString()}
|
||||
onClick={() => setHttps(true)}
|
||||
>
|
||||
HTTPS
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(!https).toString()}
|
||||
onClick={() => setHttps(false)}
|
||||
>
|
||||
HTTP
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
<Radio
|
||||
id="monitor-checks-ping"
|
||||
title="Ping monitoring"
|
||||
desc="Check whether your server is available or not."
|
||||
size="small"
|
||||
value="ping"
|
||||
checked={monitor.type === "ping"}
|
||||
onChange={(event) => handleChange(event)}
|
||||
/>
|
||||
{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 || 1}
|
||||
onChange={(event) => handleChange(event, "interval")}
|
||||
items={frequencies}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<LoadingButton
|
||||
return (
|
||||
<Box className="create-monitor">
|
||||
<Breadcrumbs
|
||||
list={[
|
||||
{ name: "monitors", path: "/monitors" },
|
||||
{ name: "create", path: `/monitors/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"
|
||||
variant="h2"
|
||||
fontSize="inherit"
|
||||
fontWeight="inherit"
|
||||
>
|
||||
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={monitor.type === "http" ? "url" : "text"}
|
||||
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 === "http"}
|
||||
onChange={(event) => handleChange(event)}
|
||||
/>
|
||||
{monitor.type === "http" ? (
|
||||
<ButtonGroup sx={{ ml: theme.spacing(16) }}>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={https.toString()}
|
||||
onClick={() => setHttps(true)}
|
||||
>
|
||||
HTTPS
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(!https).toString()}
|
||||
onClick={() => setHttps(false)}
|
||||
>
|
||||
HTTP
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
<Radio
|
||||
id="monitor-checks-ping"
|
||||
title="Ping monitoring"
|
||||
desc="Check whether your server is available or not."
|
||||
size="small"
|
||||
value="ping"
|
||||
checked={monitor.type === "ping"}
|
||||
onChange={(event) => handleChange(event)}
|
||||
/>
|
||||
{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 || 1}
|
||||
onChange={(event) => handleChange(event, "interval")}
|
||||
items={frequencies}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleCreateMonitor}
|
||||
@@ -391,10 +395,10 @@ const CreateMonitor = () => {
|
||||
>
|
||||
Create monitor
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateMonitor;
|
||||
@@ -1,135 +1,138 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
ResponsiveContainer,
|
||||
Cell,
|
||||
RadialBarChart,
|
||||
RadialBar,
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
ResponsiveContainer,
|
||||
Cell,
|
||||
RadialBarChart,
|
||||
RadialBar,
|
||||
} from "recharts";
|
||||
import { memo, useMemo, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { formatDateWithTz } from "../../../../Utils/timeUtils";
|
||||
|
||||
const CustomLabels = ({
|
||||
x,
|
||||
width,
|
||||
height,
|
||||
firstDataPoint,
|
||||
lastDataPoint,
|
||||
type,
|
||||
}) => {
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
const dateFormat = type === "day" ? "MMM D, h:mm A" : "MMM D";
|
||||
const CustomLabels = ({ x, width, height, firstDataPoint, lastDataPoint, type }) => {
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
const dateFormat = type === "day" ? "MMM D, h:mm A" : "MMM D";
|
||||
|
||||
return (
|
||||
<>
|
||||
<text x={x} y={height} dy={-3} textAnchor="start" fontSize={11}>
|
||||
{formatDateWithTz(
|
||||
new Date(firstDataPoint.time),
|
||||
dateFormat,
|
||||
uiTimezone
|
||||
)}
|
||||
</text>
|
||||
<text x={width} y={height} dy={-3} textAnchor="end" fontSize={11}>
|
||||
{formatDateWithTz(new Date(lastDataPoint.time), dateFormat, uiTimezone)}
|
||||
</text>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<text
|
||||
x={x}
|
||||
y={height}
|
||||
dy={-3}
|
||||
textAnchor="start"
|
||||
fontSize={11}
|
||||
>
|
||||
{formatDateWithTz(new Date(firstDataPoint.time), dateFormat, uiTimezone)}
|
||||
</text>
|
||||
<text
|
||||
x={width}
|
||||
y={height}
|
||||
dy={-3}
|
||||
textAnchor="end"
|
||||
fontSize={11}
|
||||
>
|
||||
{formatDateWithTz(new Date(lastDataPoint.time), dateFormat, uiTimezone)}
|
||||
</text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
CustomLabels.propTypes = {
|
||||
x: PropTypes.number.isRequired,
|
||||
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
firstDataPoint: PropTypes.object.isRequired,
|
||||
lastDataPoint: PropTypes.object.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
x: PropTypes.number.isRequired,
|
||||
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
firstDataPoint: PropTypes.object.isRequired,
|
||||
lastDataPoint: PropTypes.object.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const UpBarChart = memo(({ data, type, onBarHover }) => {
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
|
||||
const [chartHovered, setChartHovered] = useState(false);
|
||||
const [hoveredBarIndex, setHoveredBarIndex] = useState(null);
|
||||
const [chartHovered, setChartHovered] = useState(false);
|
||||
const [hoveredBarIndex, setHoveredBarIndex] = useState(null);
|
||||
|
||||
const getColorRange = (uptime) => {
|
||||
return uptime > 80
|
||||
? { 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 };
|
||||
};
|
||||
const getColorRange = (uptime) => {
|
||||
return uptime > 80
|
||||
? { 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 };
|
||||
};
|
||||
|
||||
// TODO - REMOVE THIS LATER
|
||||
const reversedData = useMemo(() => [...data].reverse(), [data]);
|
||||
// TODO - REMOVE THIS LATER
|
||||
const reversedData = useMemo(() => [...data].reverse(), [data]);
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" minWidth={210} height={155}>
|
||||
<BarChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={reversedData}
|
||||
onMouseEnter={() => {
|
||||
setChartHovered(true);
|
||||
onBarHover({ time: null, totalChecks: 0, uptimePercentage: 0 });
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setChartHovered(false);
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover(null);
|
||||
}}
|
||||
>
|
||||
<XAxis
|
||||
stroke={theme.palette.border.dark}
|
||||
height={15}
|
||||
tick={false}
|
||||
label={
|
||||
<CustomLabels
|
||||
x={0}
|
||||
y={0}
|
||||
width="100%"
|
||||
height="100%"
|
||||
firstDataPoint={reversedData[0]}
|
||||
lastDataPoint={reversedData[reversedData.length - 1]}
|
||||
type={type}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="totalChecks"
|
||||
maxBarSize={7}
|
||||
background={{ fill: "transparent" }}
|
||||
>
|
||||
{reversedData.map((entry, index) => {
|
||||
let { main, light } = getColorRange(entry.uptimePercentage);
|
||||
return (
|
||||
<Cell
|
||||
key={`cell-${entry.time}`}
|
||||
fill={
|
||||
hoveredBarIndex === index ? main : chartHovered ? light : main
|
||||
}
|
||||
onMouseEnter={() => {
|
||||
setHoveredBarIndex(index);
|
||||
onBarHover(entry);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover({
|
||||
time: null,
|
||||
totalChecks: 0,
|
||||
uptimePercentage: 0,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
return (
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
minWidth={210}
|
||||
height={155}
|
||||
>
|
||||
<BarChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={reversedData}
|
||||
onMouseEnter={() => {
|
||||
setChartHovered(true);
|
||||
onBarHover({ time: null, totalChecks: 0, uptimePercentage: 0 });
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setChartHovered(false);
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover(null);
|
||||
}}
|
||||
>
|
||||
<XAxis
|
||||
stroke={theme.palette.border.dark}
|
||||
height={15}
|
||||
tick={false}
|
||||
label={
|
||||
<CustomLabels
|
||||
x={0}
|
||||
y={0}
|
||||
width="100%"
|
||||
height="100%"
|
||||
firstDataPoint={reversedData[0]}
|
||||
lastDataPoint={reversedData[reversedData.length - 1]}
|
||||
type={type}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="totalChecks"
|
||||
maxBarSize={7}
|
||||
background={{ fill: "transparent" }}
|
||||
>
|
||||
{reversedData.map((entry, index) => {
|
||||
let { main, light } = getColorRange(entry.uptimePercentage);
|
||||
return (
|
||||
<Cell
|
||||
key={`cell-${entry.time}`}
|
||||
fill={hoveredBarIndex === index ? main : chartHovered ? light : main}
|
||||
onMouseEnter={() => {
|
||||
setHoveredBarIndex(index);
|
||||
onBarHover(entry);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover({
|
||||
time: null,
|
||||
totalChecks: 0,
|
||||
uptimePercentage: 0,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
});
|
||||
|
||||
// Add display name for the component
|
||||
@@ -137,182 +140,207 @@ UpBarChart.displayName = "UpBarChart";
|
||||
|
||||
// Validate props using PropTypes
|
||||
UpBarChart.propTypes = {
|
||||
data: PropTypes.arrayOf(PropTypes.object),
|
||||
type: PropTypes.string,
|
||||
onBarHover: PropTypes.func,
|
||||
data: PropTypes.arrayOf(PropTypes.object),
|
||||
type: PropTypes.string,
|
||||
onBarHover: PropTypes.func,
|
||||
};
|
||||
export { UpBarChart };
|
||||
|
||||
const DownBarChart = memo(({ data, type, onBarHover }) => {
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
|
||||
const [chartHovered, setChartHovered] = useState(false);
|
||||
const [hoveredBarIndex, setHoveredBarIndex] = useState(null);
|
||||
const [chartHovered, setChartHovered] = useState(false);
|
||||
const [hoveredBarIndex, setHoveredBarIndex] = useState(null);
|
||||
|
||||
// TODO - REMOVE THIS LATER
|
||||
const reversedData = useMemo(() => [...data].reverse(), [data]);
|
||||
// TODO - REMOVE THIS LATER
|
||||
const reversedData = useMemo(() => [...data].reverse(), [data]);
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" minWidth={250} height={155}>
|
||||
<BarChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={reversedData}
|
||||
onMouseEnter={() => {
|
||||
setChartHovered(true);
|
||||
onBarHover({ time: null, totalIncidents: 0 });
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setChartHovered(false);
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover(null);
|
||||
}}
|
||||
>
|
||||
<XAxis
|
||||
stroke={theme.palette.border.dark}
|
||||
height={15}
|
||||
tick={false}
|
||||
label={
|
||||
<CustomLabels
|
||||
x={0}
|
||||
y={0}
|
||||
width="100%"
|
||||
height="100%"
|
||||
firstDataPoint={reversedData[0]}
|
||||
lastDataPoint={reversedData[reversedData.length - 1]}
|
||||
type={type}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="totalIncidents"
|
||||
maxBarSize={7}
|
||||
background={{ fill: "transparent" }}
|
||||
>
|
||||
{reversedData.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${entry.time}`}
|
||||
fill={
|
||||
hoveredBarIndex === index
|
||||
? theme.palette.error.text
|
||||
: chartHovered
|
||||
? theme.palette.error.light
|
||||
: theme.palette.error.text
|
||||
}
|
||||
onMouseEnter={() => {
|
||||
setHoveredBarIndex(index);
|
||||
onBarHover(entry);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover({ time: null, totalIncidents: 0 });
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
return (
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
minWidth={250}
|
||||
height={155}
|
||||
>
|
||||
<BarChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={reversedData}
|
||||
onMouseEnter={() => {
|
||||
setChartHovered(true);
|
||||
onBarHover({ time: null, totalIncidents: 0 });
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setChartHovered(false);
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover(null);
|
||||
}}
|
||||
>
|
||||
<XAxis
|
||||
stroke={theme.palette.border.dark}
|
||||
height={15}
|
||||
tick={false}
|
||||
label={
|
||||
<CustomLabels
|
||||
x={0}
|
||||
y={0}
|
||||
width="100%"
|
||||
height="100%"
|
||||
firstDataPoint={reversedData[0]}
|
||||
lastDataPoint={reversedData[reversedData.length - 1]}
|
||||
type={type}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="totalIncidents"
|
||||
maxBarSize={7}
|
||||
background={{ fill: "transparent" }}
|
||||
>
|
||||
{reversedData.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${entry.time}`}
|
||||
fill={
|
||||
hoveredBarIndex === index
|
||||
? theme.palette.error.text
|
||||
: chartHovered
|
||||
? theme.palette.error.light
|
||||
: theme.palette.error.text
|
||||
}
|
||||
onMouseEnter={() => {
|
||||
setHoveredBarIndex(index);
|
||||
onBarHover(entry);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover({ time: null, totalIncidents: 0 });
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
});
|
||||
|
||||
DownBarChart.displayName = "DownBarChart";
|
||||
DownBarChart.propTypes = {
|
||||
data: PropTypes.arrayOf(PropTypes.object),
|
||||
type: PropTypes.string,
|
||||
onBarHover: PropTypes.func,
|
||||
data: PropTypes.arrayOf(PropTypes.object),
|
||||
type: PropTypes.string,
|
||||
onBarHover: PropTypes.func,
|
||||
};
|
||||
export { DownBarChart };
|
||||
|
||||
const ResponseGaugeChart = ({ data }) => {
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
|
||||
let max = 1000; // max ms
|
||||
let max = 1000; // max ms
|
||||
|
||||
const memoizedData = useMemo(
|
||||
() => [{ response: max, fill: "transparent", background: false }, ...data],
|
||||
[data[0].response]
|
||||
);
|
||||
const memoizedData = useMemo(
|
||||
() => [{ response: max, fill: "transparent", background: false }, ...data],
|
||||
[data[0].response]
|
||||
);
|
||||
|
||||
let responseTime = Math.floor(memoizedData[1].response);
|
||||
let responseProps =
|
||||
responseTime <= 200
|
||||
? {
|
||||
category: "Excellent",
|
||||
main: theme.palette.success.main,
|
||||
bg: theme.palette.success.bg,
|
||||
}
|
||||
: responseTime <= 500
|
||||
? {
|
||||
category: "Fair",
|
||||
main: theme.palette.success.main,
|
||||
bg: theme.palette.success.bg,
|
||||
}
|
||||
: responseTime <= 600
|
||||
? {
|
||||
category: "Acceptable",
|
||||
main: theme.palette.warning.main,
|
||||
bg: theme.palette.warning.bg,
|
||||
}
|
||||
: {
|
||||
category: "Poor",
|
||||
main: theme.palette.error.text,
|
||||
bg: theme.palette.error.bg,
|
||||
};
|
||||
let responseTime = Math.floor(memoizedData[1].response);
|
||||
let responseProps =
|
||||
responseTime <= 200
|
||||
? {
|
||||
category: "Excellent",
|
||||
main: theme.palette.success.main,
|
||||
bg: theme.palette.success.bg,
|
||||
}
|
||||
: responseTime <= 500
|
||||
? {
|
||||
category: "Fair",
|
||||
main: theme.palette.success.main,
|
||||
bg: theme.palette.success.bg,
|
||||
}
|
||||
: responseTime <= 600
|
||||
? {
|
||||
category: "Acceptable",
|
||||
main: theme.palette.warning.main,
|
||||
bg: theme.palette.warning.bg,
|
||||
}
|
||||
: {
|
||||
category: "Poor",
|
||||
main: theme.palette.error.text,
|
||||
bg: theme.palette.error.bg,
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" minWidth={210} height={155}>
|
||||
<RadialBarChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
cy="89%"
|
||||
data={memoizedData}
|
||||
startAngle={180}
|
||||
endAngle={0}
|
||||
innerRadius={100}
|
||||
outerRadius={150}
|
||||
>
|
||||
<text x={0} y="100%" dx="5%" dy={-2} textAnchor="start" fontSize={11}>
|
||||
low
|
||||
</text>
|
||||
<text x="100%" y="100%" dx="-3%" dy={-2} textAnchor="end" fontSize={11}>
|
||||
high
|
||||
</text>
|
||||
<text
|
||||
x="50%"
|
||||
y="45%"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fontSize={18}
|
||||
fontWeight={400}
|
||||
>
|
||||
{responseProps.category}
|
||||
</text>
|
||||
<text
|
||||
x="50%"
|
||||
y="55%"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="hanging"
|
||||
fontSize={25}
|
||||
>
|
||||
<tspan fontWeight={600}>{responseTime}</tspan>{" "}
|
||||
<tspan opacity={0.8}>ms</tspan>
|
||||
</text>
|
||||
<RadialBar
|
||||
background={{ fill: responseProps.bg }}
|
||||
clockWise
|
||||
dataKey="response"
|
||||
stroke="none"
|
||||
>
|
||||
<Cell fill="transparent" background={false} barSize={0} />
|
||||
<Cell fill={responseProps.main} />
|
||||
</RadialBar>
|
||||
</RadialBarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
return (
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
minWidth={210}
|
||||
height={155}
|
||||
>
|
||||
<RadialBarChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
cy="89%"
|
||||
data={memoizedData}
|
||||
startAngle={180}
|
||||
endAngle={0}
|
||||
innerRadius={100}
|
||||
outerRadius={150}
|
||||
>
|
||||
<text
|
||||
x={0}
|
||||
y="100%"
|
||||
dx="5%"
|
||||
dy={-2}
|
||||
textAnchor="start"
|
||||
fontSize={11}
|
||||
>
|
||||
low
|
||||
</text>
|
||||
<text
|
||||
x="100%"
|
||||
y="100%"
|
||||
dx="-3%"
|
||||
dy={-2}
|
||||
textAnchor="end"
|
||||
fontSize={11}
|
||||
>
|
||||
high
|
||||
</text>
|
||||
<text
|
||||
x="50%"
|
||||
y="45%"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fontSize={18}
|
||||
fontWeight={400}
|
||||
>
|
||||
{responseProps.category}
|
||||
</text>
|
||||
<text
|
||||
x="50%"
|
||||
y="55%"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="hanging"
|
||||
fontSize={25}
|
||||
>
|
||||
<tspan fontWeight={600}>{responseTime}</tspan> <tspan opacity={0.8}>ms</tspan>
|
||||
</text>
|
||||
<RadialBar
|
||||
background={{ fill: responseProps.bg }}
|
||||
clockWise
|
||||
dataKey="response"
|
||||
stroke="none"
|
||||
>
|
||||
<Cell
|
||||
fill="transparent"
|
||||
background={false}
|
||||
barSize={0}
|
||||
/>
|
||||
<Cell fill={responseProps.main} />
|
||||
</RadialBar>
|
||||
</RadialBarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
|
||||
ResponseGaugeChart.propTypes = {
|
||||
data: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
data: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
export { ResponseGaugeChart };
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
TableContainer,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
PaginationItem,
|
||||
Pagination,
|
||||
Paper,
|
||||
TableContainer,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
PaginationItem,
|
||||
Pagination,
|
||||
Paper,
|
||||
} from "@mui/material";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
@@ -21,128 +21,126 @@ import { logger } from "../../../../Utils/Logger";
|
||||
import { formatDateWithTz } from "../../../../Utils/timeUtils";
|
||||
|
||||
const PaginationTable = ({ monitorId, dateRange }) => {
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const [checks, setChecks] = useState([]);
|
||||
const [checksCount, setChecksCount] = useState(0);
|
||||
const [paginationController, setPaginationController] = useState({
|
||||
page: 0,
|
||||
rowsPerPage: 5,
|
||||
});
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const [checks, setChecks] = useState([]);
|
||||
const [checksCount, setChecksCount] = useState(0);
|
||||
const [paginationController, setPaginationController] = useState({
|
||||
page: 0,
|
||||
rowsPerPage: 5,
|
||||
});
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
useEffect(() => {
|
||||
setPaginationController((prevPaginationController) => ({
|
||||
...prevPaginationController,
|
||||
page: 0,
|
||||
}));
|
||||
}, [dateRange]);
|
||||
useEffect(() => {
|
||||
setPaginationController((prevPaginationController) => ({
|
||||
...prevPaginationController,
|
||||
page: 0,
|
||||
}));
|
||||
}, [dateRange]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPage = async () => {
|
||||
try {
|
||||
const res = await networkService.getChecksByMonitor({
|
||||
authToken: authToken,
|
||||
monitorId: monitorId,
|
||||
sortOrder: "desc",
|
||||
limit: null,
|
||||
dateRange: dateRange,
|
||||
filter: null,
|
||||
page: paginationController.page,
|
||||
rowsPerPage: paginationController.rowsPerPage,
|
||||
});
|
||||
setChecks(res.data.data.checks);
|
||||
setChecksCount(res.data.data.checksCount);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
};
|
||||
fetchPage();
|
||||
}, [
|
||||
authToken,
|
||||
monitorId,
|
||||
dateRange,
|
||||
paginationController.page,
|
||||
paginationController.rowsPerPage,
|
||||
]);
|
||||
useEffect(() => {
|
||||
const fetchPage = async () => {
|
||||
try {
|
||||
const res = await networkService.getChecksByMonitor({
|
||||
authToken: authToken,
|
||||
monitorId: monitorId,
|
||||
sortOrder: "desc",
|
||||
limit: null,
|
||||
dateRange: dateRange,
|
||||
filter: null,
|
||||
page: paginationController.page,
|
||||
rowsPerPage: paginationController.rowsPerPage,
|
||||
});
|
||||
setChecks(res.data.data.checks);
|
||||
setChecksCount(res.data.data.checksCount);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
};
|
||||
fetchPage();
|
||||
}, [
|
||||
authToken,
|
||||
monitorId,
|
||||
dateRange,
|
||||
paginationController.page,
|
||||
paginationController.rowsPerPage,
|
||||
]);
|
||||
|
||||
const handlePageChange = (_, newPage) => {
|
||||
setPaginationController({
|
||||
...paginationController,
|
||||
page: newPage - 1, // 0-indexed
|
||||
});
|
||||
};
|
||||
const handlePageChange = (_, newPage) => {
|
||||
setPaginationController({
|
||||
...paginationController,
|
||||
page: newPage - 1, // 0-indexed
|
||||
});
|
||||
};
|
||||
|
||||
let paginationComponent = <></>;
|
||||
if (checksCount > paginationController.rowsPerPage) {
|
||||
paginationComponent = (
|
||||
<Pagination
|
||||
count={Math.ceil(checksCount / paginationController.rowsPerPage)}
|
||||
page={paginationController.page + 1} //0-indexed
|
||||
onChange={handlePageChange}
|
||||
shape="rounded"
|
||||
renderItem={(item) => (
|
||||
<PaginationItem
|
||||
slots={{
|
||||
previous: ArrowBackRoundedIcon,
|
||||
next: ArrowForwardRoundedIcon,
|
||||
}}
|
||||
{...item}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
let paginationComponent = <></>;
|
||||
if (checksCount > paginationController.rowsPerPage) {
|
||||
paginationComponent = (
|
||||
<Pagination
|
||||
count={Math.ceil(checksCount / paginationController.rowsPerPage)}
|
||||
page={paginationController.page + 1} //0-indexed
|
||||
onChange={handlePageChange}
|
||||
shape="rounded"
|
||||
renderItem={(item) => (
|
||||
<PaginationItem
|
||||
slots={{
|
||||
previous: ArrowBackRoundedIcon,
|
||||
next: ArrowForwardRoundedIcon,
|
||||
}}
|
||||
{...item}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Date & Time</TableCell>
|
||||
<TableCell>Status Code</TableCell>
|
||||
<TableCell>Message</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{checks.map((check) => {
|
||||
const status = check.status === true ? "up" : "down";
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Date & Time</TableCell>
|
||||
<TableCell>Status Code</TableCell>
|
||||
<TableCell>Message</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{checks.map((check) => {
|
||||
const status = check.status === true ? "up" : "down";
|
||||
|
||||
return (
|
||||
<TableRow key={check._id}>
|
||||
<TableCell>
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatDateWithTz(
|
||||
check.createdAt,
|
||||
"ddd, MMMM D, YYYY, HH:mm A",
|
||||
uiTimezone
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{check.statusCode ? check.statusCode : "N/A"}
|
||||
</TableCell>
|
||||
<TableCell>{check.message}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{paginationComponent}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<TableRow key={check._id}>
|
||||
<TableCell>
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatDateWithTz(
|
||||
check.createdAt,
|
||||
"ddd, MMMM D, YYYY, HH:mm A",
|
||||
uiTimezone
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{check.statusCode ? check.statusCode : "N/A"}</TableCell>
|
||||
<TableCell>{check.message}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{paginationComponent}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
PaginationTable.propTypes = {
|
||||
monitorId: PropTypes.string.isRequired,
|
||||
dateRange: PropTypes.string.isRequired,
|
||||
monitorId: PropTypes.string.isRequired,
|
||||
dateRange: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default PaginationTable;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -6,68 +6,115 @@ import { Box, Skeleton, Stack, useTheme } from "@mui/material";
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Skeleton variant="rounded" width="20%" height={34} />
|
||||
<Stack gap={theme.spacing(20)} mt={theme.spacing(6)}>
|
||||
<Stack direction="row" gap={theme.spacing(4)} mt={theme.spacing(4)}>
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
style={{ minWidth: 24, minHeight: 24 }}
|
||||
/>
|
||||
<Box width="80%">
|
||||
<Skeleton variant="rounded" width="50%" height={24} />
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="50%"
|
||||
height={18}
|
||||
sx={{ mt: theme.spacing(4) }}
|
||||
/>
|
||||
</Box>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="20%"
|
||||
height={34}
|
||||
sx={{ alignSelf: "flex-end" }}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.spacing(12)}
|
||||
>
|
||||
<Skeleton variant="rounded" width="100%" height={80} />
|
||||
<Skeleton variant="rounded" width="100%" height={80} />
|
||||
<Skeleton variant="rounded" width="100%" height={80} />
|
||||
</Stack>
|
||||
<Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
mb={theme.spacing(8)}
|
||||
>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="20%"
|
||||
height={24}
|
||||
sx={{ alignSelf: "flex-end" }}
|
||||
/>
|
||||
<Skeleton variant="rounded" width="20%" height={34} />
|
||||
</Stack>
|
||||
<Box sx={{ height: "200px" }}>
|
||||
<Skeleton variant="rounded" width="100%" height="100%" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<Skeleton variant="rounded" width="20%" height={24} />
|
||||
<Skeleton variant="rounded" width="100%" height={200} />
|
||||
<Skeleton variant="rounded" width="100%" height={50} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="20%"
|
||||
height={34}
|
||||
/>
|
||||
<Stack
|
||||
gap={theme.spacing(20)}
|
||||
mt={theme.spacing(6)}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
mt={theme.spacing(4)}
|
||||
>
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
style={{ minWidth: 24, minHeight: 24 }}
|
||||
/>
|
||||
<Box width="80%">
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="50%"
|
||||
height={24}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="50%"
|
||||
height={18}
|
||||
sx={{ mt: theme.spacing(4) }}
|
||||
/>
|
||||
</Box>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="20%"
|
||||
height={34}
|
||||
sx={{ alignSelf: "flex-end" }}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.spacing(12)}
|
||||
>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={80}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={80}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={80}
|
||||
/>
|
||||
</Stack>
|
||||
<Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
mb={theme.spacing(8)}
|
||||
>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="20%"
|
||||
height={24}
|
||||
sx={{ alignSelf: "flex-end" }}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="20%"
|
||||
height={34}
|
||||
/>
|
||||
</Stack>
|
||||
<Box sx={{ height: "200px" }}>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="20%"
|
||||
height={24}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={200}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={50}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
|
||||
@@ -1,96 +1,96 @@
|
||||
import { Box, Stack, styled } from "@mui/material";
|
||||
|
||||
export const ChartBox = styled(Stack)(({ theme }) => ({
|
||||
flex: "1 30%",
|
||||
gap: theme.spacing(8),
|
||||
height: 300,
|
||||
minWidth: 250,
|
||||
padding: theme.spacing(8),
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
"& h2": {
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: 15,
|
||||
fontWeight: 500,
|
||||
},
|
||||
"& .MuiBox-root:not(.area-tooltip) p": {
|
||||
color: theme.palette.text.tertiary,
|
||||
fontSize: 13,
|
||||
},
|
||||
"& .MuiBox-root > span": {
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: 20,
|
||||
"& span": {
|
||||
opacity: 0.8,
|
||||
marginLeft: 2,
|
||||
fontSize: 15,
|
||||
},
|
||||
},
|
||||
"& .MuiStack-root": {
|
||||
flexDirection: "row",
|
||||
gap: theme.spacing(6),
|
||||
},
|
||||
"& .MuiStack-root:first-of-type": {
|
||||
alignItems: "center",
|
||||
},
|
||||
"& tspan, & text": {
|
||||
fill: theme.palette.text.tertiary,
|
||||
},
|
||||
"& path": {
|
||||
transition: "fill 300ms ease, stroke-width 400ms ease",
|
||||
},
|
||||
flex: "1 30%",
|
||||
gap: theme.spacing(8),
|
||||
height: 300,
|
||||
minWidth: 250,
|
||||
padding: theme.spacing(8),
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
"& h2": {
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: 15,
|
||||
fontWeight: 500,
|
||||
},
|
||||
"& .MuiBox-root:not(.area-tooltip) p": {
|
||||
color: theme.palette.text.tertiary,
|
||||
fontSize: 13,
|
||||
},
|
||||
"& .MuiBox-root > span": {
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: 20,
|
||||
"& span": {
|
||||
opacity: 0.8,
|
||||
marginLeft: 2,
|
||||
fontSize: 15,
|
||||
},
|
||||
},
|
||||
"& .MuiStack-root": {
|
||||
flexDirection: "row",
|
||||
gap: theme.spacing(6),
|
||||
},
|
||||
"& .MuiStack-root:first-of-type": {
|
||||
alignItems: "center",
|
||||
},
|
||||
"& tspan, & text": {
|
||||
fill: theme.palette.text.tertiary,
|
||||
},
|
||||
"& path": {
|
||||
transition: "fill 300ms ease, stroke-width 400ms ease",
|
||||
},
|
||||
}));
|
||||
|
||||
export const IconBox = styled(Box)(({ theme }) => ({
|
||||
height: 34,
|
||||
minWidth: 34,
|
||||
width: 34,
|
||||
position: "relative",
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.dark,
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
"& svg": {
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 20,
|
||||
height: 20,
|
||||
"& path": {
|
||||
stroke: theme.palette.text.tertiary,
|
||||
},
|
||||
},
|
||||
height: 34,
|
||||
minWidth: 34,
|
||||
width: 34,
|
||||
position: "relative",
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.dark,
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
"& svg": {
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 20,
|
||||
height: 20,
|
||||
"& path": {
|
||||
stroke: theme.palette.text.tertiary,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export const StatBox = styled(Box)(({ theme }) => ({
|
||||
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
|
||||
minWidth: 200,
|
||||
width: 225,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
background: `linear-gradient(340deg, ${theme.palette.background.accent} 20%, ${theme.palette.background.main} 45%)`,
|
||||
"& h2": {
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
color: theme.palette.text.secondary,
|
||||
textTransform: "uppercase",
|
||||
},
|
||||
"& p": {
|
||||
fontSize: 18,
|
||||
color: theme.palette.text.primary,
|
||||
marginTop: theme.spacing(2),
|
||||
"& span": {
|
||||
color: theme.palette.text.tertiary,
|
||||
marginLeft: theme.spacing(2),
|
||||
fontSize: 15,
|
||||
},
|
||||
},
|
||||
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
|
||||
minWidth: 200,
|
||||
width: 225,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
background: `linear-gradient(340deg, ${theme.palette.background.accent} 20%, ${theme.palette.background.main} 45%)`,
|
||||
"& h2": {
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
color: theme.palette.text.secondary,
|
||||
textTransform: "uppercase",
|
||||
},
|
||||
"& p": {
|
||||
fontSize: 18,
|
||||
color: theme.palette.text.primary,
|
||||
marginTop: theme.spacing(2),
|
||||
"& span": {
|
||||
color: theme.palette.text.tertiary,
|
||||
marginLeft: theme.spacing(2),
|
||||
fontSize: 15,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
TableContainer,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
Paper,
|
||||
Box,
|
||||
TablePagination,
|
||||
Stack,
|
||||
Typography,
|
||||
Button,
|
||||
TableContainer,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
Paper,
|
||||
Box,
|
||||
TablePagination,
|
||||
Stack,
|
||||
Typography,
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
|
||||
import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
|
||||
@@ -48,386 +48,381 @@ import useUtils from "../../utils";
|
||||
* @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));
|
||||
};
|
||||
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>
|
||||
);
|
||||
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,
|
||||
count: PropTypes.number.isRequired,
|
||||
page: PropTypes.number.isRequired,
|
||||
rowsPerPage: PropTypes.number.isRequired,
|
||||
onPageChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const MonitorTable = ({ isAdmin, filter, setLoading }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const { determineState } = useUtils();
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const { determineState } = useUtils();
|
||||
|
||||
const { rowsPerPage } = useSelector((state) => state.ui.monitors);
|
||||
const [page, setPage] = useState(0);
|
||||
const [monitors, setMonitors] = useState([]);
|
||||
const [monitorCount, setMonitorCount] = useState(0);
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const [updateTrigger, setUpdateTrigger] = useState(false);
|
||||
const [sort, setSort] = useState({});
|
||||
const prevFilter = useRef(filter);
|
||||
const { rowsPerPage } = useSelector((state) => state.ui.monitors);
|
||||
const [page, setPage] = useState(0);
|
||||
const [monitors, setMonitors] = useState([]);
|
||||
const [monitorCount, setMonitorCount] = useState(0);
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const [updateTrigger, setUpdateTrigger] = useState(false);
|
||||
const [sort, setSort] = useState({});
|
||||
const prevFilter = useRef(filter);
|
||||
|
||||
const handleActionMenuDelete = () => {
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
};
|
||||
const handleActionMenuDelete = () => {
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
};
|
||||
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
dispatch(
|
||||
setRowsPerPage({
|
||||
value: parseInt(event.target.value, 10),
|
||||
table: "monitors",
|
||||
})
|
||||
);
|
||||
setPage(0);
|
||||
};
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
dispatch(
|
||||
setRowsPerPage({
|
||||
value: parseInt(event.target.value, 10),
|
||||
table: "monitors",
|
||||
})
|
||||
);
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const fetchPage = useCallback(async () => {
|
||||
try {
|
||||
const { authToken } = authState;
|
||||
const user = jwtDecode(authToken);
|
||||
const res = await networkService.getMonitorsByTeamId({
|
||||
authToken,
|
||||
teamId: user.teamId,
|
||||
limit: 25,
|
||||
types: ["http", "ping"],
|
||||
status: null,
|
||||
checkOrder: "desc",
|
||||
normalize: true,
|
||||
page: page,
|
||||
rowsPerPage: rowsPerPage,
|
||||
filter: filter,
|
||||
field: sort.field,
|
||||
order: sort.order,
|
||||
});
|
||||
setMonitors(res?.data?.data?.monitors ?? []);
|
||||
setMonitorCount(res?.data?.data?.monitorCount ?? 0);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}, [authState, page, rowsPerPage, filter, sort, setLoading]);
|
||||
const fetchPage = useCallback(async () => {
|
||||
try {
|
||||
const { authToken } = authState;
|
||||
const user = jwtDecode(authToken);
|
||||
const res = await networkService.getMonitorsByTeamId({
|
||||
authToken,
|
||||
teamId: user.teamId,
|
||||
limit: 25,
|
||||
types: ["http", "ping"],
|
||||
status: null,
|
||||
checkOrder: "desc",
|
||||
normalize: true,
|
||||
page: page,
|
||||
rowsPerPage: rowsPerPage,
|
||||
filter: filter,
|
||||
field: sort.field,
|
||||
order: sort.order,
|
||||
});
|
||||
setMonitors(res?.data?.data?.monitors ?? []);
|
||||
setMonitorCount(res?.data?.data?.monitorCount ?? 0);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}, [authState, page, rowsPerPage, filter, sort, setLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPage();
|
||||
}, [
|
||||
updateTrigger,
|
||||
authState,
|
||||
page,
|
||||
rowsPerPage,
|
||||
filter,
|
||||
sort,
|
||||
setLoading,
|
||||
fetchPage,
|
||||
]);
|
||||
useEffect(() => {
|
||||
fetchPage();
|
||||
}, [updateTrigger, authState, page, rowsPerPage, filter, sort, setLoading, fetchPage]);
|
||||
|
||||
// Listen for changes in filter, if new value reset the page
|
||||
useEffect(() => {
|
||||
if (prevFilter.current !== filter) {
|
||||
setPage(0);
|
||||
fetchPage();
|
||||
}
|
||||
prevFilter.current = filter;
|
||||
}, [filter, fetchPage]);
|
||||
// Listen for changes in filter, if new value reset the page
|
||||
useEffect(() => {
|
||||
if (prevFilter.current !== filter) {
|
||||
setPage(0);
|
||||
fetchPage();
|
||||
}
|
||||
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}`;
|
||||
};
|
||||
/**
|
||||
* 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) {
|
||||
order = "desc";
|
||||
} else {
|
||||
order = sort.order === "asc" ? "desc" : "asc";
|
||||
}
|
||||
setSort({ field, order });
|
||||
const handleSort = async (field) => {
|
||||
let order = "";
|
||||
if (sort.field !== field) {
|
||||
order = "desc";
|
||||
} else {
|
||||
order = sort.order === "asc" ? "desc" : "asc";
|
||||
}
|
||||
setSort({ field, order });
|
||||
|
||||
const { authToken } = authState;
|
||||
const user = jwtDecode(authToken);
|
||||
const { authToken } = authState;
|
||||
const user = jwtDecode(authToken);
|
||||
|
||||
const res = await networkService.getMonitorsByTeamId({
|
||||
authToken,
|
||||
teamId: user.teamId,
|
||||
limit: 25,
|
||||
types: ["http", "ping"],
|
||||
status: null,
|
||||
checkOrder: "desc",
|
||||
normalize: true,
|
||||
page: page,
|
||||
rowsPerPage: rowsPerPage,
|
||||
filter: null,
|
||||
field: field,
|
||||
order: order,
|
||||
});
|
||||
setMonitors(res?.data?.data?.monitors ?? []);
|
||||
setMonitorCount(res?.data?.data?.monitorCount ?? 0);
|
||||
};
|
||||
const res = await networkService.getMonitorsByTeamId({
|
||||
authToken,
|
||||
teamId: user.teamId,
|
||||
limit: 25,
|
||||
types: ["http", "ping"],
|
||||
status: null,
|
||||
checkOrder: "desc",
|
||||
normalize: true,
|
||||
page: page,
|
||||
rowsPerPage: rowsPerPage,
|
||||
filter: null,
|
||||
field: field,
|
||||
order: order,
|
||||
});
|
||||
setMonitors(res?.data?.data?.monitors ?? []);
|
||||
setMonitorCount(res?.data?.data?.monitorCount ?? 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("name")}
|
||||
>
|
||||
<Box>
|
||||
Host
|
||||
<span
|
||||
style={{
|
||||
visibility: sort.field === "name" ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{sort.order === "asc" ? (
|
||||
<ArrowUpwardRoundedIcon />
|
||||
) : (
|
||||
<ArrowDownwardRoundedIcon />
|
||||
)}
|
||||
</span>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("status")}
|
||||
>
|
||||
{" "}
|
||||
<Box width="max-content">
|
||||
{" "}
|
||||
Status
|
||||
<span
|
||||
style={{
|
||||
visibility:
|
||||
sort.field === "status" ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{sort.order === "asc" ? (
|
||||
<ArrowUpwardRoundedIcon />
|
||||
) : (
|
||||
<ArrowDownwardRoundedIcon />
|
||||
)}
|
||||
</span>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>Response Time</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{monitors.map((monitor) => {
|
||||
let uptimePercentage = "";
|
||||
let percentageColor = theme.palette.percentage.uptimeExcellent;
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("name")}
|
||||
>
|
||||
<Box>
|
||||
Host
|
||||
<span
|
||||
style={{
|
||||
visibility: sort.field === "name" ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{sort.order === "asc" ? (
|
||||
<ArrowUpwardRoundedIcon />
|
||||
) : (
|
||||
<ArrowDownwardRoundedIcon />
|
||||
)}
|
||||
</span>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("status")}
|
||||
>
|
||||
{" "}
|
||||
<Box width="max-content">
|
||||
{" "}
|
||||
Status
|
||||
<span
|
||||
style={{
|
||||
visibility: sort.field === "status" ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{sort.order === "asc" ? (
|
||||
<ArrowUpwardRoundedIcon />
|
||||
) : (
|
||||
<ArrowDownwardRoundedIcon />
|
||||
)}
|
||||
</span>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>Response Time</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{monitors.map((monitor) => {
|
||||
let uptimePercentage = "";
|
||||
let percentageColor = theme.palette.percentage.uptimeExcellent;
|
||||
|
||||
// Determine uptime percentage and color based on the monitor's uptimePercentage value
|
||||
if (monitor.uptimePercentage !== undefined) {
|
||||
uptimePercentage =
|
||||
monitor.uptimePercentage === 0
|
||||
? "0"
|
||||
: (monitor.uptimePercentage * 100).toFixed(2);
|
||||
// Determine uptime percentage and color based on the monitor's uptimePercentage value
|
||||
if (monitor.uptimePercentage !== undefined) {
|
||||
uptimePercentage =
|
||||
monitor.uptimePercentage === 0
|
||||
? "0"
|
||||
: (monitor.uptimePercentage * 100).toFixed(2);
|
||||
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
const params = {
|
||||
url: monitor.url,
|
||||
title: monitor.name,
|
||||
percentage: uptimePercentage,
|
||||
percentageColor,
|
||||
status: determineState(monitor),
|
||||
};
|
||||
const params = {
|
||||
url: monitor.url,
|
||||
title: monitor.name,
|
||||
percentage: uptimePercentage,
|
||||
percentageColor,
|
||||
status: determineState(monitor),
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={monitor._id}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
navigate(`/monitors/${monitor._id}`);
|
||||
}}
|
||||
>
|
||||
<TableCell>
|
||||
<Host key={monitor._id} params={params} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusLabel
|
||||
status={params.status}
|
||||
text={params.status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<BarChart checks={monitor.checks.slice().reverse()} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span style={{ textTransform: "uppercase" }}>
|
||||
{monitor.type}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ActionsMenu
|
||||
monitor={monitor}
|
||||
isAdmin={isAdmin}
|
||||
updateCallback={handleActionMenuDelete}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<TableRow
|
||||
key={monitor._id}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
navigate(`/monitors/${monitor._id}`);
|
||||
}}
|
||||
>
|
||||
<TableCell>
|
||||
<Host
|
||||
key={monitor._id}
|
||||
params={params}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusLabel
|
||||
status={params.status}
|
||||
text={params.status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<BarChart checks={monitor.checks.slice().reverse()} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span style={{ textTransform: "uppercase" }}>{monitor.type}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ActionsMenu
|
||||
monitor={monitor}
|
||||
isAdmin={isAdmin}
|
||||
updateCallback={handleActionMenuDelete}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MonitorTable.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
filter: PropTypes.string,
|
||||
setLoading: PropTypes.func,
|
||||
isAdmin: PropTypes.bool,
|
||||
filter: PropTypes.string,
|
||||
setLoading: PropTypes.func,
|
||||
};
|
||||
|
||||
const MemoizedMonitorTable = memo(MonitorTable);
|
||||
|
||||
@@ -6,106 +6,106 @@ import Background from "../../../assets/Images/background-grid.svg?react";
|
||||
import ClockSnooze from "../../../assets/icons/clock-snooze.svg?react";
|
||||
|
||||
const StatusBox = ({ title, value }) => {
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
|
||||
let sharedStyles = {
|
||||
position: "absolute",
|
||||
right: 8,
|
||||
opacity: 0.5,
|
||||
"& svg path": { stroke: theme.palette.other.icon },
|
||||
};
|
||||
let sharedStyles = {
|
||||
position: "absolute",
|
||||
right: 8,
|
||||
opacity: 0.5,
|
||||
"& svg path": { stroke: theme.palette.other.icon },
|
||||
};
|
||||
|
||||
let color;
|
||||
let icon;
|
||||
if (title === "up") {
|
||||
color = theme.palette.success.main;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: 8 }}>
|
||||
<Arrow />
|
||||
</Box>
|
||||
);
|
||||
} else if (title === "down") {
|
||||
color = theme.palette.error.text;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, transform: "rotate(180deg)", top: 5 }}>
|
||||
<Arrow />
|
||||
</Box>
|
||||
);
|
||||
} else if (title === "paused") {
|
||||
color = theme.palette.warning.main;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: 12, right: 12 }}>
|
||||
<ClockSnooze />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
let color;
|
||||
let icon;
|
||||
if (title === "up") {
|
||||
color = theme.palette.success.main;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: 8 }}>
|
||||
<Arrow />
|
||||
</Box>
|
||||
);
|
||||
} else if (title === "down") {
|
||||
color = theme.palette.error.text;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, transform: "rotate(180deg)", top: 5 }}>
|
||||
<Arrow />
|
||||
</Box>
|
||||
);
|
||||
} else if (title === "paused") {
|
||||
color = theme.palette.warning.main;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: 12, right: 12 }}>
|
||||
<ClockSnooze />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
position="relative"
|
||||
flex={1}
|
||||
border={1}
|
||||
borderColor={theme.palette.border.light}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
backgroundColor={theme.palette.background.main}
|
||||
p={theme.spacing(8)}
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "-10%",
|
||||
left: "5%",
|
||||
pointerEvents: "none",
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.other.grid,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background />
|
||||
</Box>
|
||||
<Box
|
||||
textTransform="uppercase"
|
||||
fontSize={15}
|
||||
letterSpacing={0.5}
|
||||
color={theme.palette.text.secondary}
|
||||
mb={theme.spacing(8)}
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
{title}
|
||||
</Box>
|
||||
{icon}
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="flex-start"
|
||||
fontSize={36}
|
||||
fontWeight={600}
|
||||
color={color}
|
||||
gap="2px"
|
||||
>
|
||||
{value}
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize={20}
|
||||
fontWeight={300}
|
||||
color={theme.palette.text.secondary}
|
||||
sx={{ opacity: 0.3 }}
|
||||
>
|
||||
#
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
return (
|
||||
<Box
|
||||
position="relative"
|
||||
flex={1}
|
||||
border={1}
|
||||
borderColor={theme.palette.border.light}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
backgroundColor={theme.palette.background.main}
|
||||
p={theme.spacing(8)}
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "-10%",
|
||||
left: "5%",
|
||||
pointerEvents: "none",
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.other.grid,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background />
|
||||
</Box>
|
||||
<Box
|
||||
textTransform="uppercase"
|
||||
fontSize={15}
|
||||
letterSpacing={0.5}
|
||||
color={theme.palette.text.secondary}
|
||||
mb={theme.spacing(8)}
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
{title}
|
||||
</Box>
|
||||
{icon}
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="flex-start"
|
||||
fontSize={36}
|
||||
fontWeight={600}
|
||||
color={color}
|
||||
gap="2px"
|
||||
>
|
||||
{value}
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize={20}
|
||||
fontWeight={300}
|
||||
color={theme.palette.text.secondary}
|
||||
sx={{ opacity: 0.3 }}
|
||||
>
|
||||
#
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
StatusBox.propTypes = {
|
||||
title: PropTypes.oneOf(["up", "down", "paused"]).isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
title: PropTypes.oneOf(["up", "down", "paused"]).isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
};
|
||||
|
||||
export default StatusBox;
|
||||
|
||||
@@ -4,226 +4,222 @@ import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { IconButton, Menu, MenuItem } from "@mui/material";
|
||||
import {
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem
|
||||
} from "@mui/material";
|
||||
import {
|
||||
deleteUptimeMonitor,
|
||||
pauseUptimeMonitor,
|
||||
getUptimeMonitorsByTeamId,
|
||||
deleteUptimeMonitor,
|
||||
pauseUptimeMonitor,
|
||||
getUptimeMonitorsByTeamId,
|
||||
} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import Settings from "../../../assets/icons/settings-bold.svg?react";
|
||||
import PropTypes from "prop-types";
|
||||
import Dialog from "../../../Components/Dialog"
|
||||
import Dialog from "../../../Components/Dialog";
|
||||
|
||||
const ActionsMenu = ({ monitor, isAdmin, updateCallback }) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [actions, setActions] = useState({});
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const authToken = authState.authToken;
|
||||
const { isLoading } = useSelector((state) => state.uptimeMonitors);
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [actions, setActions] = useState({});
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const authToken = authState.authToken;
|
||||
const { isLoading } = useSelector((state) => state.uptimeMonitors);
|
||||
|
||||
const handleRemove = async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
let monitor = { _id: actions.id };
|
||||
const action = await dispatch(
|
||||
deleteUptimeMonitor({ authToken: authState.authToken, monitor })
|
||||
);
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
setIsOpen(false); // close modal
|
||||
dispatch(getUptimeMonitorsByTeamId(authState.authToken));
|
||||
updateCallback();
|
||||
createToast({ body: "Monitor deleted successfully." });
|
||||
} else {
|
||||
createToast({ body: "Failed to delete monitor." });
|
||||
}
|
||||
};
|
||||
const handleRemove = async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
let monitor = { _id: actions.id };
|
||||
const action = await dispatch(
|
||||
deleteUptimeMonitor({ authToken: authState.authToken, monitor })
|
||||
);
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
setIsOpen(false); // close modal
|
||||
dispatch(getUptimeMonitorsByTeamId(authState.authToken));
|
||||
updateCallback();
|
||||
createToast({ body: "Monitor deleted successfully." });
|
||||
} else {
|
||||
createToast({ body: "Failed to delete monitor." });
|
||||
}
|
||||
};
|
||||
|
||||
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." });
|
||||
}
|
||||
};
|
||||
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." });
|
||||
}
|
||||
};
|
||||
|
||||
const openMenu = (event, id, url) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setAnchorEl(event.currentTarget);
|
||||
setActions({ id: id, url: url });
|
||||
};
|
||||
const openMenu = (event, id, url) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setAnchorEl(event.currentTarget);
|
||||
setActions({ id: id, url: url });
|
||||
};
|
||||
|
||||
const openRemove = (e) => {
|
||||
closeMenu(e);
|
||||
setIsOpen(true);
|
||||
};
|
||||
const openRemove = (e) => {
|
||||
closeMenu(e);
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const closeMenu = (e) => {
|
||||
e.stopPropagation();
|
||||
setAnchorEl(null);
|
||||
};
|
||||
const closeMenu = (e) => {
|
||||
e.stopPropagation();
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
aria-label="monitor actions"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
openMenu(
|
||||
event,
|
||||
monitor._id,
|
||||
monitor.type === "ping" ? null : monitor.url
|
||||
);
|
||||
}}
|
||||
sx={{
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"& svg path": {
|
||||
stroke: theme.palette.other.icon,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Settings />
|
||||
</IconButton>
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
aria-label="monitor actions"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
openMenu(event, monitor._id, monitor.type === "ping" ? null : monitor.url);
|
||||
}}
|
||||
sx={{
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"& svg path": {
|
||||
stroke: theme.palette.other.icon,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Settings />
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
className="actions-menu"
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={(e) => closeMenu(e)}
|
||||
disableScrollLock
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
"& ul": { p: theme.spacing(2.5) },
|
||||
"& li": { m: 0 },
|
||||
"& li:last-of-type": {
|
||||
color: theme.palette.error.text,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{actions.url !== null ? (
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
closeMenu(e);
|
||||
e.stopPropagation();
|
||||
window.open(actions.url, "_blank", "noreferrer");
|
||||
}}
|
||||
>
|
||||
Open site
|
||||
</MenuItem>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(`/monitors/${actions.id}`);
|
||||
}}
|
||||
>
|
||||
Details
|
||||
</MenuItem>
|
||||
{/* TODO - pass monitor id to Incidents page */}
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(`/incidents/${actions.id}`);
|
||||
}}
|
||||
>
|
||||
Incidents
|
||||
</MenuItem>
|
||||
{isAdmin && (
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
<Menu
|
||||
className="actions-menu"
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={(e) => closeMenu(e)}
|
||||
disableScrollLock
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
"& ul": { p: theme.spacing(2.5) },
|
||||
"& li": { m: 0 },
|
||||
"& li:last-of-type": {
|
||||
color: theme.palette.error.text,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{actions.url !== null ? (
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
closeMenu(e);
|
||||
e.stopPropagation();
|
||||
window.open(actions.url, "_blank", "noreferrer");
|
||||
}}
|
||||
>
|
||||
Open site
|
||||
</MenuItem>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(`/monitors/${actions.id}`);
|
||||
}}
|
||||
>
|
||||
Details
|
||||
</MenuItem>
|
||||
{/* TODO - pass monitor id to Incidents page */}
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(`/incidents/${actions.id}`);
|
||||
}}
|
||||
>
|
||||
Incidents
|
||||
</MenuItem>
|
||||
{isAdmin && (
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
navigate(`/monitors/configure/${actions.id}`);
|
||||
}}
|
||||
>
|
||||
Configure
|
||||
</MenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(`/monitors/create/${actions.id}`);
|
||||
}}
|
||||
>
|
||||
Clone
|
||||
</MenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePause(e);
|
||||
}}
|
||||
>
|
||||
{monitor?.isActive === true ? "Pause" : "Resume"}
|
||||
|
||||
</MenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openRemove(e);
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
<Dialog
|
||||
modelTitle="modal-delete-monitor"
|
||||
modelDescription="delete-monitor-confirmation"
|
||||
open={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
title="Do you really want to delete this monitor?"
|
||||
confirmationBtnLbl="Delete"
|
||||
confirmationBtnOnClick={e => { e.stopPropagation(); handleRemove(e) }}
|
||||
cancelBtnLbl="Cancel"
|
||||
cancelBtnOnClick={e => { e.stopPropagation(); setIsOpen(false) }}
|
||||
theme={theme}
|
||||
isLoading={isLoading}
|
||||
description="Once deleted, this monitor cannot be retrieved."
|
||||
>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
navigate(`/monitors/configure/${actions.id}`);
|
||||
}}
|
||||
>
|
||||
Configure
|
||||
</MenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(`/monitors/create/${actions.id}`);
|
||||
}}
|
||||
>
|
||||
Clone
|
||||
</MenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePause(e);
|
||||
}}
|
||||
>
|
||||
{monitor?.isActive === true ? "Pause" : "Resume"}
|
||||
</MenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openRemove(e);
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
<Dialog
|
||||
modelTitle="modal-delete-monitor"
|
||||
modelDescription="delete-monitor-confirmation"
|
||||
open={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
title="Do you really want to delete this monitor?"
|
||||
confirmationBtnLbl="Delete"
|
||||
confirmationBtnOnClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRemove(e);
|
||||
}}
|
||||
cancelBtnLbl="Cancel"
|
||||
cancelBtnOnClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
theme={theme}
|
||||
isLoading={isLoading}
|
||||
description="Once deleted, this monitor cannot be retrieved."
|
||||
></Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ActionsMenu.propTypes = {
|
||||
monitor: PropTypes.shape({
|
||||
_id: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
isActive: PropTypes.bool,
|
||||
}).isRequired,
|
||||
isAdmin: PropTypes.bool,
|
||||
updateCallback: PropTypes.func,
|
||||
monitor: PropTypes.shape({
|
||||
_id: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
isActive: PropTypes.bool,
|
||||
}).isRequired,
|
||||
isAdmin: PropTypes.bool,
|
||||
updateCallback: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ActionsMenu;
|
||||
|
||||
@@ -7,49 +7,53 @@ import PlaceholderDark from "../../../assets/Images/data_placeholder_dark.svg?re
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const Fallback = ({ isAdmin }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
alignItems="center"
|
||||
backgroundColor={theme.palette.background.main}
|
||||
p={theme.spacing(30)}
|
||||
pt={theme.spacing(25)}
|
||||
gap={theme.spacing(2)}
|
||||
border={1}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
borderColor={theme.palette.border.light}
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
<Box pb={theme.spacing(20)}>
|
||||
{mode === "light" ? <PlaceholderLight /> : <PlaceholderDark />}
|
||||
</Box>
|
||||
<Typography component="h2" variant="h2" fontWeight={500}>
|
||||
No monitors found to display
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
It looks like you don’t have any monitors set up yet.
|
||||
</Typography>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
navigate("/monitors/create");
|
||||
}}
|
||||
sx={{ mt: theme.spacing(12) }}
|
||||
>
|
||||
Create your first monitor
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
alignItems="center"
|
||||
backgroundColor={theme.palette.background.main}
|
||||
p={theme.spacing(30)}
|
||||
pt={theme.spacing(25)}
|
||||
gap={theme.spacing(2)}
|
||||
border={1}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
borderColor={theme.palette.border.light}
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
<Box pb={theme.spacing(20)}>
|
||||
{mode === "light" ? <PlaceholderLight /> : <PlaceholderDark />}
|
||||
</Box>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
fontWeight={500}
|
||||
>
|
||||
No monitors found to display
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
It looks like you don’t have any monitors set up yet.
|
||||
</Typography>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
navigate("/monitors/create");
|
||||
}}
|
||||
sx={{ mt: theme.spacing(12) }}
|
||||
>
|
||||
Create your first monitor
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Fallback.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
isAdmin: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Fallback;
|
||||
|
||||
@@ -12,50 +12,50 @@ import PropTypes from "prop-types";
|
||||
* @returns {React.ElementType} Returns a div element with the host details.
|
||||
*/
|
||||
const Host = ({ params }) => {
|
||||
return (
|
||||
<Box className="host">
|
||||
<Box
|
||||
display="inline-block"
|
||||
position="relative"
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
"&:before": {
|
||||
position: "absolute",
|
||||
content: `""`,
|
||||
width: "4px",
|
||||
height: "4px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "gray",
|
||||
opacity: 0.8,
|
||||
right: "-10px",
|
||||
top: "42%",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{params.title}
|
||||
</Box>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
color: params.percentageColor,
|
||||
fontWeight: 500,
|
||||
ml: "15px",
|
||||
}}
|
||||
>
|
||||
{params.percentage}%
|
||||
</Typography>
|
||||
<Box sx={{ opacity: 0.6 }}>{params.url}</Box>
|
||||
</Box>
|
||||
);
|
||||
return (
|
||||
<Box className="host">
|
||||
<Box
|
||||
display="inline-block"
|
||||
position="relative"
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
"&:before": {
|
||||
position: "absolute",
|
||||
content: `""`,
|
||||
width: "4px",
|
||||
height: "4px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "gray",
|
||||
opacity: 0.8,
|
||||
right: "-10px",
|
||||
top: "42%",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{params.title}
|
||||
</Box>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
color: params.percentageColor,
|
||||
fontWeight: 500,
|
||||
ml: "15px",
|
||||
}}
|
||||
>
|
||||
{params.percentage}%
|
||||
</Typography>
|
||||
<Box sx={{ opacity: 0.6 }}>{params.url}</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Host.propTypes = {
|
||||
params: PropTypes.shape({
|
||||
title: PropTypes.string,
|
||||
percentageColor: PropTypes.string,
|
||||
percentage: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
}).isRequired,
|
||||
params: PropTypes.shape({
|
||||
title: PropTypes.string,
|
||||
percentageColor: PropTypes.string,
|
||||
percentage: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default Host;
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
.monitors .MuiStack-root > button:not(.MuiIconButton-root) {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
height: var(--env-var-height-2);
|
||||
min-width: fit-content;
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
height: var(--env-var-height-2);
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.current-monitors-counter {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
min-width: 25px;
|
||||
min-height: 25px;
|
||||
border-radius: 50%;
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
font-weight: 500;
|
||||
margin-left: 10px;
|
||||
line-height: 0.8;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
min-width: 25px;
|
||||
min-height: 25px;
|
||||
border-radius: 50%;
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
font-weight: 500;
|
||||
margin-left: 10px;
|
||||
line-height: 0.8;
|
||||
}
|
||||
|
||||
@@ -4,13 +4,7 @@ import { useSelector, useDispatch } from "react-redux";
|
||||
import { getUptimeMonitorsByTeamId } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Stack,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { Box, Button, CircularProgress, Stack, Typography } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import Fallback from "./fallback";
|
||||
@@ -22,173 +16,174 @@ import Search from "../../../Components/Inputs/Search";
|
||||
import useDebounce from "../../../Utils/debounce";
|
||||
|
||||
const Monitors = ({ isAdmin }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const monitorState = useSelector((state) => state.uptimeMonitors);
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const [search, setSearch] = useState("");
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const dispatch = useDispatch({});
|
||||
const debouncedFilter = useDebounce(search, 500);
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const monitorState = useSelector((state) => state.uptimeMonitors);
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const [search, setSearch] = useState("");
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const dispatch = useDispatch({});
|
||||
const debouncedFilter = useDebounce(search, 500);
|
||||
|
||||
const handleSearch = (value) => {
|
||||
setIsSearching(true);
|
||||
setSearch(value);
|
||||
};
|
||||
const handleSearch = (value) => {
|
||||
setIsSearching(true);
|
||||
setSearch(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getUptimeMonitorsByTeamId(authState.authToken));
|
||||
}, [authState.authToken, dispatch]);
|
||||
useEffect(() => {
|
||||
dispatch(getUptimeMonitorsByTeamId(authState.authToken));
|
||||
}, [authState.authToken, dispatch]);
|
||||
|
||||
let loading =
|
||||
monitorState?.isLoading &&
|
||||
monitorState?.monitorsSummary?.monitors?.length === 0;
|
||||
let loading =
|
||||
monitorState?.isLoading && monitorState?.monitorsSummary?.monitors?.length === 0;
|
||||
|
||||
return (
|
||||
<Stack className="monitors table-container" gap={theme.spacing(8)}>
|
||||
{loading ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
<>
|
||||
<Box>
|
||||
<Breadcrumbs list={[{ name: `monitors`, path: "/monitors" }]} />
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
mt={theme.spacing(5)}
|
||||
>
|
||||
<Greeting type="uptime" />
|
||||
{isAdmin &&
|
||||
monitorState?.monitorsSummary?.monitors?.length !== 0 && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
navigate("/monitors/create");
|
||||
}}
|
||||
sx={{ fontWeight: 500 }}
|
||||
>
|
||||
Create monitor
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
{isAdmin && monitorState?.monitorsSummary?.monitors?.length === 0 && (
|
||||
<Fallback isAdmin={isAdmin} />
|
||||
)}
|
||||
return (
|
||||
<Stack
|
||||
className="monitors table-container"
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
{loading ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
<>
|
||||
<Box>
|
||||
<Breadcrumbs list={[{ name: `monitors`, path: "/monitors" }]} />
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
mt={theme.spacing(5)}
|
||||
>
|
||||
<Greeting type="uptime" />
|
||||
{isAdmin && monitorState?.monitorsSummary?.monitors?.length !== 0 && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
navigate("/monitors/create");
|
||||
}}
|
||||
sx={{ fontWeight: 500 }}
|
||||
>
|
||||
Create monitor
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
{isAdmin && monitorState?.monitorsSummary?.monitors?.length === 0 && (
|
||||
<Fallback isAdmin={isAdmin} />
|
||||
)}
|
||||
|
||||
{monitorState?.monitorsSummary?.monitors?.length !== 0 && (
|
||||
<>
|
||||
<Stack
|
||||
gap={theme.spacing(8)}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<StatusBox
|
||||
title="up"
|
||||
value={monitorState?.monitorsSummary?.monitorCounts?.up ?? 0}
|
||||
/>
|
||||
<StatusBox
|
||||
title="down"
|
||||
value={
|
||||
monitorState?.monitorsSummary?.monitorCounts?.down ?? 0
|
||||
}
|
||||
/>
|
||||
<StatusBox
|
||||
title="paused"
|
||||
value={
|
||||
monitorState?.monitorsSummary?.monitorCounts?.paused ?? 0
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<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>
|
||||
<Box
|
||||
className="current-monitors-counter"
|
||||
color={theme.palette.text.primary}
|
||||
border={1}
|
||||
borderColor={theme.palette.border.light}
|
||||
backgroundColor={theme.palette.background.accent}
|
||||
>
|
||||
{monitorState?.monitorsSummary?.monitorCounts?.total || 0}
|
||||
</Box>
|
||||
<Box width="25%" minWidth={150} ml="auto">
|
||||
<Search
|
||||
options={monitorState?.monitorsSummary?.monitors ?? []}
|
||||
filteredBy="name"
|
||||
inputValue={search}
|
||||
handleInputChange={handleSearch}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Box position="relative">
|
||||
{isSearching && (
|
||||
<>
|
||||
<Box
|
||||
width="100%"
|
||||
height="100%"
|
||||
position="absolute"
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.main,
|
||||
opacity: 0.8,
|
||||
zIndex: 100,
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
height="100%"
|
||||
position="absolute"
|
||||
top="20%"
|
||||
left="50%"
|
||||
sx={{
|
||||
transform: "translateX(-50%)",
|
||||
zIndex: 101,
|
||||
}}
|
||||
>
|
||||
<CircularProgress
|
||||
sx={{
|
||||
color: theme.palette.other.icon,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
<MonitorTable
|
||||
isAdmin={isAdmin}
|
||||
filter={debouncedFilter}
|
||||
setLoading={setIsSearching}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
{monitorState?.monitorsSummary?.monitors?.length !== 0 && (
|
||||
<>
|
||||
<Stack
|
||||
gap={theme.spacing(8)}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<StatusBox
|
||||
title="up"
|
||||
value={monitorState?.monitorsSummary?.monitorCounts?.up ?? 0}
|
||||
/>
|
||||
<StatusBox
|
||||
title="down"
|
||||
value={monitorState?.monitorsSummary?.monitorCounts?.down ?? 0}
|
||||
/>
|
||||
<StatusBox
|
||||
title="paused"
|
||||
value={monitorState?.monitorsSummary?.monitorCounts?.paused ?? 0}
|
||||
/>
|
||||
</Stack>
|
||||
<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>
|
||||
<Box
|
||||
className="current-monitors-counter"
|
||||
color={theme.palette.text.primary}
|
||||
border={1}
|
||||
borderColor={theme.palette.border.light}
|
||||
backgroundColor={theme.palette.background.accent}
|
||||
>
|
||||
{monitorState?.monitorsSummary?.monitorCounts?.total || 0}
|
||||
</Box>
|
||||
<Box
|
||||
width="25%"
|
||||
minWidth={150}
|
||||
ml="auto"
|
||||
>
|
||||
<Search
|
||||
options={monitorState?.monitorsSummary?.monitors ?? []}
|
||||
filteredBy="name"
|
||||
inputValue={search}
|
||||
handleInputChange={handleSearch}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Box position="relative">
|
||||
{isSearching && (
|
||||
<>
|
||||
<Box
|
||||
width="100%"
|
||||
height="100%"
|
||||
position="absolute"
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.main,
|
||||
opacity: 0.8,
|
||||
zIndex: 100,
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
height="100%"
|
||||
position="absolute"
|
||||
top="20%"
|
||||
left="50%"
|
||||
sx={{
|
||||
transform: "translateX(-50%)",
|
||||
zIndex: 101,
|
||||
}}
|
||||
>
|
||||
<CircularProgress
|
||||
sx={{
|
||||
color: theme.palette.other.icon,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
<MonitorTable
|
||||
isAdmin={isAdmin}
|
||||
filter={debouncedFilter}
|
||||
setLoading={setIsSearching}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Monitors.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
isAdmin: PropTypes.bool,
|
||||
};
|
||||
export default Monitors;
|
||||
|
||||
@@ -7,26 +7,55 @@ import { useTheme } from "@emotion/react";
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Skeleton variant="rounded" width="50%" height={36} />
|
||||
<Skeleton variant="rounded" width="15%" height={36} />
|
||||
</Stack>
|
||||
<Stack
|
||||
gap={theme.spacing(12)}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Skeleton variant="rounded" width="100%" height={100} />
|
||||
<Skeleton variant="rounded" width="100%" height={100} />
|
||||
<Skeleton variant="rounded" width="100%" height={100} />
|
||||
</Stack>
|
||||
<Skeleton variant="rounded" width="100%" height="100%" flex={1} />
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="50%"
|
||||
height={36}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="15%"
|
||||
height={36}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
gap={theme.spacing(12)}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={100}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={100}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={100}
|
||||
/>
|
||||
</Stack>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height="100%"
|
||||
flex={1}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
import { Stack, styled } from "@mui/material";
|
||||
|
||||
export const ConfigBox = styled(Stack)(({ theme }) => ({
|
||||
flexDirection: "row",
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.spacing(2),
|
||||
backgroundColor: theme.palette.background.main,
|
||||
"& > *": {
|
||||
paddingTop: theme.spacing(12),
|
||||
paddingBottom: theme.spacing(18),
|
||||
},
|
||||
"& > div:first-of-type": {
|
||||
flex: 0.7,
|
||||
borderRight: 1,
|
||||
borderRightStyle: "solid",
|
||||
borderRightColor: theme.palette.border.light,
|
||||
paddingRight: theme.spacing(15),
|
||||
paddingLeft: theme.spacing(15),
|
||||
},
|
||||
"& > div:last-of-type": {
|
||||
flex: 1,
|
||||
paddingRight: theme.spacing(20),
|
||||
paddingLeft: theme.spacing(18),
|
||||
},
|
||||
"& h2": {
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: 15,
|
||||
fontWeight: 600,
|
||||
},
|
||||
"& h3, & p": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
"& p": {
|
||||
fontSize: 13,
|
||||
},
|
||||
flexDirection: "row",
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.spacing(2),
|
||||
backgroundColor: theme.palette.background.main,
|
||||
"& > *": {
|
||||
paddingTop: theme.spacing(12),
|
||||
paddingBottom: theme.spacing(18),
|
||||
},
|
||||
"& > div:first-of-type": {
|
||||
flex: 0.7,
|
||||
borderRight: 1,
|
||||
borderRightStyle: "solid",
|
||||
borderRightColor: theme.palette.border.light,
|
||||
paddingRight: theme.spacing(15),
|
||||
paddingLeft: theme.spacing(15),
|
||||
},
|
||||
"& > div:last-of-type": {
|
||||
flex: 1,
|
||||
paddingRight: theme.spacing(20),
|
||||
paddingLeft: theme.spacing(18),
|
||||
},
|
||||
"& h2": {
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: 15,
|
||||
fontWeight: 600,
|
||||
},
|
||||
"& h3, & p": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
"& p": {
|
||||
fontSize: 13,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1,88 +1,88 @@
|
||||
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";
|
||||
};
|
||||
const theme = useTheme();
|
||||
const determineState = (monitor) => {
|
||||
if (monitor.isActive === false) return "paused";
|
||||
if (monitor?.status === undefined) return "pending";
|
||||
return monitor?.status == true ? "up" : "down";
|
||||
};
|
||||
|
||||
const statusColor = {
|
||||
up: theme.palette.success.main,
|
||||
down: theme.palette.error.main,
|
||||
paused: theme.palette.warning.main,
|
||||
pending: theme.palette.warning.main,
|
||||
};
|
||||
const statusColor = {
|
||||
up: theme.palette.success.main,
|
||||
down: theme.palette.error.main,
|
||||
paused: theme.palette.warning.main,
|
||||
pending: theme.palette.warning.main,
|
||||
};
|
||||
|
||||
const statusMsg = {
|
||||
up: "Your site is up.",
|
||||
down: "Your site is down.",
|
||||
paused: "Pending...",
|
||||
};
|
||||
const statusMsg = {
|
||||
up: "Your site is up.",
|
||||
down: "Your site is down.",
|
||||
paused: "Pending...",
|
||||
};
|
||||
|
||||
const pagespeedStatusMsg = {
|
||||
up: "Live (collecting data)",
|
||||
down: "Inactive",
|
||||
paused: "Paused",
|
||||
};
|
||||
const statusStyles = {
|
||||
up: {
|
||||
backgroundColor: theme.palette.success.bg,
|
||||
background: `linear-gradient(340deg, ${theme.palette.success.light} -60%, ${theme.palette.success.bg} 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%)`,
|
||||
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%)`,
|
||||
borderColor: theme.palette.warning.light,
|
||||
"& h2": { color: theme.palette.warning.main },
|
||||
},
|
||||
pending: {
|
||||
backgroundColor: theme.palette.warning.light,
|
||||
background: `linear-gradient(340deg, ${theme.palette.warning.dark} -60%, ${theme.palette.warning.light} 35%)`,
|
||||
borderColor: theme.palette.warning.dark,
|
||||
"& h2": { color: theme.palette.warning.main },
|
||||
},
|
||||
};
|
||||
const pagespeedStyles = {
|
||||
up: {
|
||||
bg: theme.palette.success.bg,
|
||||
light: theme.palette.success.light,
|
||||
stroke: theme.palette.success.main,
|
||||
},
|
||||
down: {
|
||||
bg: theme.palette.error.bg,
|
||||
light: theme.palette.error.light,
|
||||
stroke: theme.palette.error.main,
|
||||
},
|
||||
paused: {
|
||||
bg: theme.palette.warning.bg,
|
||||
light: theme.palette.warning.light,
|
||||
stroke: theme.palette.warning.main,
|
||||
},
|
||||
pending: {
|
||||
bg: theme.palette.warning.bg,
|
||||
light: theme.palette.warning.light,
|
||||
stroke: theme.palette.warning.main,
|
||||
},
|
||||
};
|
||||
const pagespeedStatusMsg = {
|
||||
up: "Live (collecting data)",
|
||||
down: "Inactive",
|
||||
paused: "Paused",
|
||||
};
|
||||
const statusStyles = {
|
||||
up: {
|
||||
backgroundColor: theme.palette.success.bg,
|
||||
background: `linear-gradient(340deg, ${theme.palette.success.light} -60%, ${theme.palette.success.bg} 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%)`,
|
||||
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%)`,
|
||||
borderColor: theme.palette.warning.light,
|
||||
"& h2": { color: theme.palette.warning.main },
|
||||
},
|
||||
pending: {
|
||||
backgroundColor: theme.palette.warning.light,
|
||||
background: `linear-gradient(340deg, ${theme.palette.warning.dark} -60%, ${theme.palette.warning.light} 35%)`,
|
||||
borderColor: theme.palette.warning.dark,
|
||||
"& h2": { color: theme.palette.warning.main },
|
||||
},
|
||||
};
|
||||
const pagespeedStyles = {
|
||||
up: {
|
||||
bg: theme.palette.success.bg,
|
||||
light: theme.palette.success.light,
|
||||
stroke: theme.palette.success.main,
|
||||
},
|
||||
down: {
|
||||
bg: theme.palette.error.bg,
|
||||
light: theme.palette.error.light,
|
||||
stroke: theme.palette.error.main,
|
||||
},
|
||||
paused: {
|
||||
bg: theme.palette.warning.bg,
|
||||
light: theme.palette.warning.light,
|
||||
stroke: theme.palette.warning.main,
|
||||
},
|
||||
pending: {
|
||||
bg: theme.palette.warning.bg,
|
||||
light: theme.palette.warning.light,
|
||||
stroke: theme.palette.warning.main,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
determineState,
|
||||
statusColor,
|
||||
statusMsg,
|
||||
pagespeedStatusMsg,
|
||||
pagespeedStyles,
|
||||
statusStyles,
|
||||
};
|
||||
return {
|
||||
determineState,
|
||||
statusColor,
|
||||
statusMsg,
|
||||
pagespeedStatusMsg,
|
||||
pagespeedStyles,
|
||||
statusStyles,
|
||||
};
|
||||
};
|
||||
|
||||
export default useUtils;
|
||||
|
||||
@@ -10,8 +10,8 @@ import { useTheme } from "@emotion/react";
|
||||
* So That why we're using JavaScript default parameters instead.
|
||||
*/
|
||||
const DefaultValue = {
|
||||
title: "Oh no! You dropped your sushi!",
|
||||
desc: "Either the URL doesn’t exist, or you don’t have access to it.",
|
||||
title: "Oh no! You dropped your sushi!",
|
||||
desc: "Either the URL doesn’t exist, or you don’t have access to it.",
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -31,33 +31,47 @@ const DefaultValue = {
|
||||
* @returns {JSX.Element} The rendered error page component.
|
||||
*/
|
||||
const NotFound = ({ title = DefaultValue.title, desc = DefaultValue.desc }) => {
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Stack height="100vh" justifyContent="center">
|
||||
<Stack gap={theme.spacing(2)} alignItems="center">
|
||||
<img src={NotFoundSvg} alt="404" style={{ maxHeight: "25rem" }} />
|
||||
<Typography component="h1" variant="h1" fontSize={16}>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography variant="body1">{desc}</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ mt: theme.spacing(10) }}
|
||||
onClick={() => navigate("/")}
|
||||
>
|
||||
Go to the main dashboard
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
height="100vh"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
alignItems="center"
|
||||
>
|
||||
<img
|
||||
src={NotFoundSvg}
|
||||
alt="404"
|
||||
style={{ maxHeight: "25rem" }}
|
||||
/>
|
||||
<Typography
|
||||
component="h1"
|
||||
variant="h1"
|
||||
fontSize={16}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography variant="body1">{desc}</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ mt: theme.spacing(10) }}
|
||||
onClick={() => navigate("/")}
|
||||
>
|
||||
Go to the main dashboard
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
NotFound.propTypes = {
|
||||
title: PropTypes.string,
|
||||
desc: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
desc: PropTypes.string,
|
||||
};
|
||||
|
||||
export default NotFound;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
.configure-pagespeed button {
|
||||
height: var(--env-var-height-2);
|
||||
height: var(--env-var-height-2);
|
||||
}
|
||||
|
||||
.configure-pagespeed .field,
|
||||
.configure-pagespeed .section-disabled,
|
||||
.configure-pagespeed .select-wrapper {
|
||||
flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ import { Box, Button, Modal, Stack, Tooltip, Typography } from "@mui/material";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import {
|
||||
deletePageSpeed,
|
||||
getPagespeedMonitorById,
|
||||
getPageSpeedByTeamId,
|
||||
updatePageSpeed,
|
||||
pausePageSpeed,
|
||||
deletePageSpeed,
|
||||
getPagespeedMonitorById,
|
||||
getPageSpeedByTeamId,
|
||||
updatePageSpeed,
|
||||
pausePageSpeed,
|
||||
} from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
|
||||
import { monitorValidation } from "../../../Validation/validation";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
@@ -27,456 +27,472 @@ import useUtils from "../../Monitors/utils";
|
||||
import "./index.css";
|
||||
|
||||
const PageSpeedConfigure = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
const { isLoading } = useSelector((state) => state.pageSpeedMonitors);
|
||||
const { monitorId } = useParams();
|
||||
const [monitor, setMonitor] = useState({});
|
||||
const [errors, setErrors] = useState({});
|
||||
const { statusColor, pagespeedStatusMsg, determineState } = useUtils();
|
||||
const idMap = {
|
||||
"monitor-url": "url",
|
||||
"monitor-name": "name",
|
||||
"monitor-checks-http": "type",
|
||||
"monitor-checks-ping": "type",
|
||||
"notify-email-default": "notification-email",
|
||||
};
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
const { isLoading } = useSelector((state) => state.pageSpeedMonitors);
|
||||
const { monitorId } = useParams();
|
||||
const [monitor, setMonitor] = useState({});
|
||||
const [errors, setErrors] = useState({});
|
||||
const { statusColor, pagespeedStatusMsg, determineState } = useUtils();
|
||||
const idMap = {
|
||||
"monitor-url": "url",
|
||||
"monitor-name": "name",
|
||||
"monitor-checks-http": "type",
|
||||
"monitor-checks-ping": "type",
|
||||
"notify-email-default": "notification-email",
|
||||
};
|
||||
|
||||
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" },
|
||||
];
|
||||
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" },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMonitor = async () => {
|
||||
try {
|
||||
const action = await dispatch(
|
||||
getPagespeedMonitorById({ authToken, monitorId })
|
||||
);
|
||||
useEffect(() => {
|
||||
const fetchMonitor = async () => {
|
||||
try {
|
||||
const action = await dispatch(getPagespeedMonitorById({ authToken, monitorId }));
|
||||
|
||||
if (getPagespeedMonitorById.fulfilled.match(action)) {
|
||||
const monitor = action.payload.data;
|
||||
setMonitor(monitor);
|
||||
} else if (getPagespeedMonitorById.rejected.match(action)) {
|
||||
throw new Error(action.error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error fetching monitor of id: " + monitorId);
|
||||
navigate("/not-found", { replace: true });
|
||||
}
|
||||
};
|
||||
fetchMonitor();
|
||||
}, [dispatch, authToken, monitorId, navigate]);
|
||||
if (getPagespeedMonitorById.fulfilled.match(action)) {
|
||||
const monitor = action.payload.data;
|
||||
setMonitor(monitor);
|
||||
} else if (getPagespeedMonitorById.rejected.match(action)) {
|
||||
throw new Error(action.error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error fetching monitor of id: " + monitorId);
|
||||
navigate("/not-found", { replace: true });
|
||||
}
|
||||
};
|
||||
fetchMonitor();
|
||||
}, [dispatch, authToken, monitorId, navigate]);
|
||||
|
||||
const handleChange = (event, name) => {
|
||||
let { value, id } = event.target;
|
||||
if (!name) name = idMap[id];
|
||||
const handleChange = (event, name) => {
|
||||
let { 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 {
|
||||
if (name === "interval") {
|
||||
value = value * MS_PER_MINUTE;
|
||||
}
|
||||
setMonitor((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
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 {
|
||||
if (name === "interval") {
|
||||
value = value * MS_PER_MINUTE;
|
||||
}
|
||||
setMonitor((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
|
||||
const validation = monitorValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
const validation = monitorValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
|
||||
if (validation.error)
|
||||
updatedErrors[name] = validation.error.details[0].message;
|
||||
else delete updatedErrors[name];
|
||||
return updatedErrors;
|
||||
});
|
||||
}
|
||||
};
|
||||
if (validation.error) updatedErrors[name] = validation.error.details[0].message;
|
||||
else delete updatedErrors[name];
|
||||
return updatedErrors;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handlePause = async () => {
|
||||
try {
|
||||
const action = await dispatch(pausePageSpeed({ authToken, monitorId }));
|
||||
if (pausePageSpeed.fulfilled.match(action)) {
|
||||
const monitor = action.payload.data;
|
||||
setMonitor(monitor);
|
||||
} else if (pausePageSpeed.rejected.match(action)) {
|
||||
throw new Error(action.error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error pausing monitor: " + monitorId);
|
||||
createToast({ body: "Failed to pause monitor" });
|
||||
}
|
||||
};
|
||||
const handlePause = async () => {
|
||||
try {
|
||||
const action = await dispatch(pausePageSpeed({ authToken, monitorId }));
|
||||
if (pausePageSpeed.fulfilled.match(action)) {
|
||||
const monitor = action.payload.data;
|
||||
setMonitor(monitor);
|
||||
} else if (pausePageSpeed.rejected.match(action)) {
|
||||
throw new Error(action.error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error pausing monitor: " + monitorId);
|
||||
createToast({ body: "Failed to pause monitor" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async (event) => {
|
||||
event.preventDefault();
|
||||
const action = await dispatch(
|
||||
updatePageSpeed({ authToken, monitor: monitor })
|
||||
);
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
createToast({ body: "Monitor updated successfully!" });
|
||||
dispatch(getPageSpeedByTeamId(authToken));
|
||||
} else {
|
||||
createToast({ body: "Failed to update monitor." });
|
||||
}
|
||||
};
|
||||
const handleSave = async (event) => {
|
||||
event.preventDefault();
|
||||
const action = await dispatch(updatePageSpeed({ authToken, monitor: monitor }));
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
createToast({ body: "Monitor updated successfully!" });
|
||||
dispatch(getPageSpeedByTeamId(authToken));
|
||||
} else {
|
||||
createToast({ body: "Failed to update monitor." });
|
||||
}
|
||||
};
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const handleRemove = async (event) => {
|
||||
event.preventDefault();
|
||||
const action = await dispatch(deletePageSpeed({ authToken, monitor }));
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
navigate("/pagespeed");
|
||||
} else {
|
||||
createToast({ body: "Failed to delete monitor." });
|
||||
}
|
||||
};
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const handleRemove = async (event) => {
|
||||
event.preventDefault();
|
||||
const action = await dispatch(deletePageSpeed({ authToken, monitor }));
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
navigate("/pagespeed");
|
||||
} else {
|
||||
createToast({ body: "Failed to delete monitor." });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack className="configure-pagespeed" gap={theme.spacing(10)}>
|
||||
{Object.keys(monitor).length === 0 ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
<>
|
||||
<Breadcrumbs
|
||||
list={[
|
||||
{ name: "pagespeed", path: "/pagespeed" },
|
||||
{ name: "details", path: `/pagespeed/${monitorId}` },
|
||||
{ name: "configure", path: `/pagespeed/configure/${monitorId}` },
|
||||
]}
|
||||
/>
|
||||
<Stack
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
onSubmit={handleSave}
|
||||
flex={1}
|
||||
gap={theme.spacing(10)}
|
||||
>
|
||||
<Stack direction="row" gap={theme.spacing(2)}>
|
||||
<Box>
|
||||
<Typography component="h1" variant="h1">
|
||||
{monitor.name}
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
height="fit-content"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Tooltip
|
||||
title={pagespeedStatusMsg[determineState(monitor)]}
|
||||
disableInteractive
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -8],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<PulseDot color={statusColor[determineState(monitor)]} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Typography component="h2" variant="h2">
|
||||
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
|
||||
</Typography>
|
||||
<Typography
|
||||
position="relative"
|
||||
variant="body2"
|
||||
ml={theme.spacing(6)}
|
||||
mt={theme.spacing(1)}
|
||||
sx={{
|
||||
"&:before": {
|
||||
position: "absolute",
|
||||
content: `""`,
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: theme.palette.text.tertiary,
|
||||
opacity: 0.8,
|
||||
left: -10,
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
Editing...
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box alignSelf="flex-end" ml="auto">
|
||||
<LoadingButton
|
||||
onClick={handlePause}
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
sx={{
|
||||
pl: theme.spacing(4),
|
||||
pr: theme.spacing(6),
|
||||
"& svg": {
|
||||
mr: theme.spacing(2),
|
||||
"& path": {
|
||||
stroke: theme.palette.other.icon,
|
||||
strokeWidth: 0.1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{monitor?.isActive ? (
|
||||
<>
|
||||
<PauseCircleOutlineIcon />
|
||||
Pause
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PlayCircleOutlineRoundedIcon />
|
||||
Resume
|
||||
</>
|
||||
)}
|
||||
</LoadingButton>
|
||||
<LoadingButton
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => setIsOpen(true)}
|
||||
sx={{
|
||||
ml: theme.spacing(6),
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
<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(20)}
|
||||
sx={{
|
||||
".MuiInputBase-root:has(> .Mui-disabled)": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Field
|
||||
type="url"
|
||||
id="monitor-url"
|
||||
label="URL"
|
||||
placeholder="random.website.com"
|
||||
value={monitor?.url?.replace("http://", "") || ""}
|
||||
onChange={handleChange}
|
||||
error={errors.url}
|
||||
disabled={true}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="monitor-name"
|
||||
label="Monitor display name"
|
||||
placeholder="Example monitor"
|
||||
isOptional={true}
|
||||
value={monitor?.name || ""}
|
||||
onChange={handleChange}
|
||||
error={errors.name}
|
||||
/>
|
||||
</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"
|
||||
) || false
|
||||
}
|
||||
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(20)}>
|
||||
<Select
|
||||
id="monitor-frequency"
|
||||
label="Check frequency"
|
||||
items={frequencies}
|
||||
value={monitor?.interval / MS_PER_MINUTE || 3}
|
||||
onChange={(event) => handleChange(event, "interval")}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Stack direction="row" justifyContent="flex-end" mt="auto">
|
||||
<LoadingButton
|
||||
loading={isLoading}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleSave}
|
||||
sx={{ px: theme.spacing(12) }}
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
<Modal
|
||||
aria-labelledby="modal-delete-pagespeed-monitor"
|
||||
aria-describedby="delete-pagespeed-monitor-confirmation"
|
||||
open={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
disablePortal
|
||||
>
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 400,
|
||||
bgcolor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxShadow: 24,
|
||||
p: theme.spacing(15),
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
id="modal-delete-pagespeed-monitor"
|
||||
component="h2"
|
||||
variant="h2"
|
||||
fontWeight={500}
|
||||
>
|
||||
Do you really want to delete this monitor?
|
||||
</Typography>
|
||||
<Typography
|
||||
id="delete-pagespeed-monitor-confirmation"
|
||||
variant="body1"
|
||||
>
|
||||
Once deleted, this monitor cannot be retrieved.
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
mt={theme.spacing(12)}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="contained" color="error" onClick={handleRemove}>
|
||||
Delete
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
className="configure-pagespeed"
|
||||
gap={theme.spacing(10)}
|
||||
>
|
||||
{Object.keys(monitor).length === 0 ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
<>
|
||||
<Breadcrumbs
|
||||
list={[
|
||||
{ name: "pagespeed", path: "/pagespeed" },
|
||||
{ name: "details", path: `/pagespeed/${monitorId}` },
|
||||
{ name: "configure", path: `/pagespeed/configure/${monitorId}` },
|
||||
]}
|
||||
/>
|
||||
<Stack
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
onSubmit={handleSave}
|
||||
flex={1}
|
||||
gap={theme.spacing(10)}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Box>
|
||||
<Typography
|
||||
component="h1"
|
||||
variant="h1"
|
||||
>
|
||||
{monitor.name}
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
height="fit-content"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Tooltip
|
||||
title={pagespeedStatusMsg[determineState(monitor)]}
|
||||
disableInteractive
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -8],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<PulseDot color={statusColor[determineState(monitor)]} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
|
||||
</Typography>
|
||||
<Typography
|
||||
position="relative"
|
||||
variant="body2"
|
||||
ml={theme.spacing(6)}
|
||||
mt={theme.spacing(1)}
|
||||
sx={{
|
||||
"&:before": {
|
||||
position: "absolute",
|
||||
content: `""`,
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: theme.palette.text.tertiary,
|
||||
opacity: 0.8,
|
||||
left: -10,
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
Editing...
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box
|
||||
alignSelf="flex-end"
|
||||
ml="auto"
|
||||
>
|
||||
<LoadingButton
|
||||
onClick={handlePause}
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
sx={{
|
||||
pl: theme.spacing(4),
|
||||
pr: theme.spacing(6),
|
||||
"& svg": {
|
||||
mr: theme.spacing(2),
|
||||
"& path": {
|
||||
stroke: theme.palette.other.icon,
|
||||
strokeWidth: 0.1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{monitor?.isActive ? (
|
||||
<>
|
||||
<PauseCircleOutlineIcon />
|
||||
Pause
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PlayCircleOutlineRoundedIcon />
|
||||
Resume
|
||||
</>
|
||||
)}
|
||||
</LoadingButton>
|
||||
<LoadingButton
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => setIsOpen(true)}
|
||||
sx={{
|
||||
ml: theme.spacing(6),
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
<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(20)}
|
||||
sx={{
|
||||
".MuiInputBase-root:has(> .Mui-disabled)": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Field
|
||||
type="url"
|
||||
id="monitor-url"
|
||||
label="URL"
|
||||
placeholder="random.website.com"
|
||||
value={monitor?.url?.replace("http://", "") || ""}
|
||||
onChange={handleChange}
|
||||
error={errors.url}
|
||||
disabled={true}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="monitor-name"
|
||||
label="Monitor display name"
|
||||
placeholder="Example monitor"
|
||||
isOptional={true}
|
||||
value={monitor?.name || ""}
|
||||
onChange={handleChange}
|
||||
error={errors.name}
|
||||
/>
|
||||
</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"
|
||||
) || false
|
||||
}
|
||||
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(20)}>
|
||||
<Select
|
||||
id="monitor-frequency"
|
||||
label="Check frequency"
|
||||
items={frequencies}
|
||||
value={monitor?.interval / MS_PER_MINUTE || 3}
|
||||
onChange={(event) => handleChange(event, "interval")}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
mt="auto"
|
||||
>
|
||||
<LoadingButton
|
||||
loading={isLoading}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleSave}
|
||||
sx={{ px: theme.spacing(12) }}
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
<Modal
|
||||
aria-labelledby="modal-delete-pagespeed-monitor"
|
||||
aria-describedby="delete-pagespeed-monitor-confirmation"
|
||||
open={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
disablePortal
|
||||
>
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 400,
|
||||
bgcolor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxShadow: 24,
|
||||
p: theme.spacing(15),
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
id="modal-delete-pagespeed-monitor"
|
||||
component="h2"
|
||||
variant="h2"
|
||||
fontWeight={500}
|
||||
>
|
||||
Do you really want to delete this monitor?
|
||||
</Typography>
|
||||
<Typography
|
||||
id="delete-pagespeed-monitor-confirmation"
|
||||
variant="body1"
|
||||
>
|
||||
Once deleted, this monitor cannot be retrieved.
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
mt={theme.spacing(12)}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={handleRemove}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageSpeedConfigure;
|
||||
|
||||
@@ -7,45 +7,80 @@ import { useTheme } from "@emotion/react";
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Skeleton variant="rounded" width="15%" height={34} />
|
||||
<Stack gap={theme.spacing(20)} mt={theme.spacing(6)} maxWidth="1000px">
|
||||
<Stack direction="row" gap={theme.spacing(4)} mt={theme.spacing(4)}>
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
style={{ minWidth: 24, minHeight: 24 }}
|
||||
/>
|
||||
<Box width="80%">
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="50%"
|
||||
height={24}
|
||||
sx={{ mb: theme.spacing(4) }}
|
||||
/>
|
||||
<Skeleton variant="rounded" width="50%" height={18} />
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(6)}
|
||||
sx={{
|
||||
ml: "auto",
|
||||
alignSelf: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Skeleton variant="rounded" width={100} height={34} />
|
||||
<Skeleton variant="rounded" width={100} height={34} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Skeleton variant="rounded" width="100%" height={500} />
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Skeleton variant="rounded" width="15%" height={34} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="15%"
|
||||
height={34}
|
||||
/>
|
||||
<Stack
|
||||
gap={theme.spacing(20)}
|
||||
mt={theme.spacing(6)}
|
||||
maxWidth="1000px"
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
mt={theme.spacing(4)}
|
||||
>
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
style={{ minWidth: 24, minHeight: 24 }}
|
||||
/>
|
||||
<Box width="80%">
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="50%"
|
||||
height={24}
|
||||
sx={{ mb: theme.spacing(4) }}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="50%"
|
||||
height={18}
|
||||
/>
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(6)}
|
||||
sx={{
|
||||
ml: "auto",
|
||||
alignSelf: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width={100}
|
||||
height={34}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width={100}
|
||||
height={34}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={500}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="15%"
|
||||
height={34}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
|
||||
@@ -25,86 +25,86 @@ const CreatePageSpeed = () => {
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
|
||||
const idMap = {
|
||||
"monitor-url": "url",
|
||||
"monitor-name": "name",
|
||||
"monitor-checks-http": "type",
|
||||
"monitor-checks-ping": "type",
|
||||
"notify-email-default": "notification-email",
|
||||
};
|
||||
const idMap = {
|
||||
"monitor-url": "url",
|
||||
"monitor-name": "name",
|
||||
"monitor-checks-http": "type",
|
||||
"monitor-checks-ping": "type",
|
||||
"notify-email-default": "notification-email",
|
||||
};
|
||||
|
||||
const [monitor, setMonitor] = useState({
|
||||
url: "",
|
||||
name: "",
|
||||
type: "pagespeed",
|
||||
notifications: [],
|
||||
interval: 3,
|
||||
});
|
||||
const [https, setHttps] = useState(true);
|
||||
const [errors, setErrors] = useState({});
|
||||
const [monitor, setMonitor] = useState({
|
||||
url: "",
|
||||
name: "",
|
||||
type: "pagespeed",
|
||||
notifications: [],
|
||||
interval: 3,
|
||||
});
|
||||
const [https, setHttps] = useState(true);
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const handleChange = (event, name) => {
|
||||
const { value, id } = event.target;
|
||||
if (!name) name = idMap[id];
|
||||
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,
|
||||
}));
|
||||
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 }
|
||||
);
|
||||
const { error } = monitorValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
if (error) updatedErrors[name] = error.details[0].message;
|
||||
else delete updatedErrors[name];
|
||||
return updatedErrors;
|
||||
});
|
||||
}
|
||||
};
|
||||
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: `http${https ? "s" : ""}://` + monitor.url,
|
||||
name: monitor.name === "" ? monitor.url : monitor.name,
|
||||
type: monitor.type,
|
||||
interval: monitor.interval * MS_PER_MINUTE,
|
||||
};
|
||||
const handleCreateMonitor = async (event) => {
|
||||
event.preventDefault();
|
||||
//obj to submit
|
||||
let form = {
|
||||
url: `http${https ? "s" : ""}://` + 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,
|
||||
});
|
||||
const { error } = monitorValidation.validate(form, {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
Tooltip,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
Text,
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
Tooltip,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
Text,
|
||||
} from "recharts";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useMemo, useState } from "react";
|
||||
@@ -15,26 +15,26 @@ import { formatDateWithTz } from "../../../../Utils/timeUtils";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const config = {
|
||||
seo: {
|
||||
id: "seo",
|
||||
text: "SEO",
|
||||
color: "unresolved",
|
||||
},
|
||||
performance: {
|
||||
id: "performance",
|
||||
text: "performance",
|
||||
color: "success",
|
||||
},
|
||||
bestPractices: {
|
||||
id: "bestPractices",
|
||||
text: "best practices",
|
||||
color: "warning",
|
||||
},
|
||||
accessibility: {
|
||||
id: "accessibility",
|
||||
text: "accessibility",
|
||||
color: "primary",
|
||||
},
|
||||
seo: {
|
||||
id: "seo",
|
||||
text: "SEO",
|
||||
color: "unresolved",
|
||||
},
|
||||
performance: {
|
||||
id: "performance",
|
||||
text: "performance",
|
||||
color: "success",
|
||||
},
|
||||
bestPractices: {
|
||||
id: "bestPractices",
|
||||
text: "best practices",
|
||||
color: "warning",
|
||||
},
|
||||
accessibility: {
|
||||
id: "accessibility",
|
||||
text: "accessibility",
|
||||
color: "primary",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -47,81 +47,79 @@ const config = {
|
||||
*/
|
||||
|
||||
const CustomToolTip = ({ active, payload, label, config }) => {
|
||||
const theme = useTheme();
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
const theme = useTheme();
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<Box
|
||||
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>
|
||||
{Object.keys(config)
|
||||
.reverse()
|
||||
.map((key) => {
|
||||
const { color } = config[key];
|
||||
const dotColor = theme.palette[color].main;
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<Box
|
||||
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>
|
||||
{Object.keys(config)
|
||||
.reverse()
|
||||
.map((key) => {
|
||||
const { color } = config[key];
|
||||
const dotColor = theme.palette[color].main;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
key={`${key}-tooltip`}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(3)}
|
||||
mt={theme.spacing(1)}
|
||||
sx={{
|
||||
"& span": {
|
||||
color: theme.palette.text.tertiary,
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
width={theme.spacing(4)}
|
||||
height={theme.spacing(4)}
|
||||
backgroundColor={dotColor}
|
||||
sx={{ borderRadius: "50%" }}
|
||||
/>
|
||||
<Typography
|
||||
component="span"
|
||||
textTransform="capitalize"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
{config[key].text}
|
||||
</Typography>{" "}
|
||||
<Typography component="span">
|
||||
{payload[0].payload[key]}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
return (
|
||||
<Stack
|
||||
key={`${key}-tooltip`}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(3)}
|
||||
mt={theme.spacing(1)}
|
||||
sx={{
|
||||
"& span": {
|
||||
color: theme.palette.text.tertiary,
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
width={theme.spacing(4)}
|
||||
height={theme.spacing(4)}
|
||||
backgroundColor={dotColor}
|
||||
sx={{ borderRadius: "50%" }}
|
||||
/>
|
||||
<Typography
|
||||
component="span"
|
||||
textTransform="capitalize"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
{config[key].text}
|
||||
</Typography>{" "}
|
||||
<Typography component="span">{payload[0].payload[key]}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
CustomToolTip.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
payload: PropTypes.array,
|
||||
label: PropTypes.string,
|
||||
config: PropTypes.object,
|
||||
active: PropTypes.bool,
|
||||
payload: PropTypes.array,
|
||||
label: PropTypes.string,
|
||||
config: PropTypes.object,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -131,38 +129,38 @@ CustomToolTip.propTypes = {
|
||||
* @returns {Array} The formatted data with gaps.
|
||||
*/
|
||||
const processDataWithGaps = (data, interval) => {
|
||||
if (data.length === 0) return [];
|
||||
let formattedData = [];
|
||||
let last = new Date(data[0].createdAt).getTime();
|
||||
if (data.length === 0) return [];
|
||||
let formattedData = [];
|
||||
let last = new Date(data[0].createdAt).getTime();
|
||||
|
||||
// Helper function to add a null entry
|
||||
const addNullEntry = (timestamp) => {
|
||||
formattedData.push({
|
||||
accessibility: "N/A",
|
||||
bestPractices: "N/A",
|
||||
performance: "N/A",
|
||||
seo: "N/A",
|
||||
createdAt: timestamp,
|
||||
});
|
||||
};
|
||||
// Helper function to add a null entry
|
||||
const addNullEntry = (timestamp) => {
|
||||
formattedData.push({
|
||||
accessibility: "N/A",
|
||||
bestPractices: "N/A",
|
||||
performance: "N/A",
|
||||
seo: "N/A",
|
||||
createdAt: timestamp,
|
||||
});
|
||||
};
|
||||
|
||||
data.forEach((entry) => {
|
||||
const current = new Date(entry.createdAt).getTime();
|
||||
data.forEach((entry) => {
|
||||
const current = new Date(entry.createdAt).getTime();
|
||||
|
||||
if (current - last > interval * 2) {
|
||||
// Insert null entries for each interval
|
||||
let temp = last + interval;
|
||||
while (temp < current) {
|
||||
addNullEntry(new Date(temp).toISOString());
|
||||
temp += interval;
|
||||
}
|
||||
}
|
||||
if (current - last > interval * 2) {
|
||||
// Insert null entries for each interval
|
||||
let temp = last + interval;
|
||||
while (temp < current) {
|
||||
addNullEntry(new Date(temp).toISOString());
|
||||
temp += interval;
|
||||
}
|
||||
}
|
||||
|
||||
formattedData.push(entry);
|
||||
last = current;
|
||||
});
|
||||
formattedData.push(entry);
|
||||
last = current;
|
||||
});
|
||||
|
||||
return formattedData;
|
||||
return formattedData;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -177,32 +175,32 @@ const processDataWithGaps = (data, interval) => {
|
||||
* @returns {JSX.Element|null} The tick element or null if the tick should be hidden.
|
||||
*/
|
||||
const CustomTick = ({ x, y, payload, index }) => {
|
||||
const theme = useTheme();
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
const theme = useTheme();
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
// Render nothing for the first tick
|
||||
if (index === 0) return null;
|
||||
// Render nothing for the first tick
|
||||
if (index === 0) return null;
|
||||
|
||||
return (
|
||||
<Text
|
||||
x={x}
|
||||
y={y + 8}
|
||||
textAnchor="middle"
|
||||
fill={theme.palette.text.tertiary}
|
||||
fontSize={11}
|
||||
fontWeight={400}
|
||||
>
|
||||
{formatDateWithTz(payload?.value, "h:mm a", uiTimezone)}
|
||||
</Text>
|
||||
);
|
||||
return (
|
||||
<Text
|
||||
x={x}
|
||||
y={y + 8}
|
||||
textAnchor="middle"
|
||||
fill={theme.palette.text.tertiary}
|
||||
fontSize={11}
|
||||
fontWeight={400}
|
||||
>
|
||||
{formatDateWithTz(payload?.value, "h:mm a", uiTimezone)}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
CustomTick.propTypes = {
|
||||
x: PropTypes.number,
|
||||
y: PropTypes.number,
|
||||
payload: PropTypes.shape({
|
||||
value: PropTypes.string.isRequired,
|
||||
}),
|
||||
index: PropTypes.number,
|
||||
x: PropTypes.number,
|
||||
y: PropTypes.number,
|
||||
payload: PropTypes.shape({
|
||||
value: PropTypes.string.isRequired,
|
||||
}),
|
||||
index: PropTypes.number,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -214,102 +212,115 @@ CustomTick.propTypes = {
|
||||
*/
|
||||
|
||||
const PagespeedDetailsAreaChart = ({ data, interval, metrics }) => {
|
||||
const theme = useTheme();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const memoizedData = useMemo(
|
||||
() => processDataWithGaps(data, interval),
|
||||
[data[0]]
|
||||
);
|
||||
const theme = useTheme();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const memoizedData = useMemo(() => processDataWithGaps(data, interval), [data[0]]);
|
||||
|
||||
const filteredConfig = Object.keys(config).reduce((result, key) => {
|
||||
if (metrics[key]) {
|
||||
result[key] = config[key];
|
||||
}
|
||||
return result;
|
||||
}, {});
|
||||
const filteredConfig = Object.keys(config).reduce((result, key) => {
|
||||
if (metrics[key]) {
|
||||
result[key] = config[key];
|
||||
}
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" minWidth={25} height={215}>
|
||||
<AreaChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={memoizedData}
|
||||
margin={{ top: 10 }}
|
||||
onMouseMove={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<CartesianGrid
|
||||
stroke={theme.palette.border.light}
|
||||
strokeWidth={1}
|
||||
strokeOpacity={1}
|
||||
fill="transparent"
|
||||
vertical={false}
|
||||
/>
|
||||
<XAxis
|
||||
stroke={theme.palette.border.dark}
|
||||
dataKey="createdAt"
|
||||
tick={<CustomTick />}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
height={18}
|
||||
minTickGap={0}
|
||||
interval="equidistantPreserveStart"
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={{ stroke: theme.palette.border.light }}
|
||||
content={<CustomToolTip config={filteredConfig} />}
|
||||
/>
|
||||
<defs>
|
||||
{Object.values(filteredConfig).map(({ id, color }) => {
|
||||
const startColor = theme.palette[color].main;
|
||||
const endColor = theme.palette[color].light;
|
||||
return (
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
minWidth={25}
|
||||
height={215}
|
||||
>
|
||||
<AreaChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={memoizedData}
|
||||
margin={{ top: 10 }}
|
||||
onMouseMove={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<CartesianGrid
|
||||
stroke={theme.palette.border.light}
|
||||
strokeWidth={1}
|
||||
strokeOpacity={1}
|
||||
fill="transparent"
|
||||
vertical={false}
|
||||
/>
|
||||
<XAxis
|
||||
stroke={theme.palette.border.dark}
|
||||
dataKey="createdAt"
|
||||
tick={<CustomTick />}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
height={18}
|
||||
minTickGap={0}
|
||||
interval="equidistantPreserveStart"
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={{ stroke: theme.palette.border.light }}
|
||||
content={<CustomToolTip config={filteredConfig} />}
|
||||
/>
|
||||
<defs>
|
||||
{Object.values(filteredConfig).map(({ id, color }) => {
|
||||
const startColor = theme.palette[color].main;
|
||||
const endColor = theme.palette[color].light;
|
||||
|
||||
return (
|
||||
<linearGradient id={id} x1="0" y1="0" x2="0" y2="1" key={id}>
|
||||
<stop offset="0%" stopColor={startColor} stopOpacity={0.8} />
|
||||
<stop offset="100%" stopColor={endColor} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
);
|
||||
})}
|
||||
</defs>
|
||||
{Object.keys(filteredConfig).map((key) => {
|
||||
const { color } = filteredConfig[key];
|
||||
const strokeColor = theme.palette[color].main;
|
||||
const bgColor = theme.palette.background.main;
|
||||
return (
|
||||
<linearGradient
|
||||
id={id}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
key={id}
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={startColor}
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={endColor}
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</linearGradient>
|
||||
);
|
||||
})}
|
||||
</defs>
|
||||
{Object.keys(filteredConfig).map((key) => {
|
||||
const { color } = filteredConfig[key];
|
||||
const strokeColor = theme.palette[color].main;
|
||||
const bgColor = theme.palette.background.main;
|
||||
|
||||
return (
|
||||
<Area
|
||||
connectNulls
|
||||
key={key}
|
||||
dataKey={key}
|
||||
stackId={1}
|
||||
stroke={strokeColor}
|
||||
strokeWidth={isHovered ? 2.5 : 1.5}
|
||||
fill={`url(#${filteredConfig[key].id})`}
|
||||
activeDot={{ stroke: bgColor, fill: strokeColor, r: 4.5 }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
return (
|
||||
<Area
|
||||
connectNulls
|
||||
key={key}
|
||||
dataKey={key}
|
||||
stackId={1}
|
||||
stroke={strokeColor}
|
||||
strokeWidth={isHovered ? 2.5 : 1.5}
|
||||
fill={`url(#${filteredConfig[key].id})`}
|
||||
activeDot={{ stroke: bgColor, fill: strokeColor, r: 4.5 }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
|
||||
PagespeedDetailsAreaChart.propTypes = {
|
||||
data: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
createdAt: PropTypes.string.isRequired,
|
||||
accessibility: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
||||
.isRequired,
|
||||
bestPractices: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
||||
.isRequired,
|
||||
performance: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
||||
.isRequired,
|
||||
seo: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
interval: PropTypes.number.isRequired,
|
||||
metrics: PropTypes.object,
|
||||
data: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
createdAt: PropTypes.string.isRequired,
|
||||
accessibility: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
bestPractices: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
performance: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
seo: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
interval: PropTypes.number.isRequired,
|
||||
metrics: PropTypes.object,
|
||||
};
|
||||
|
||||
export default PagespeedDetailsAreaChart;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user