Compare commits

..
296 Commits
Author SHA1 Message Date
Alexander Holliday 13d40e7cbb Merge pull request #1468 from bluewave-labs/revert-1467-hotfix/be/move-middleware
Revert "hotfix: move middleware to after JobQueue initialization"
2024-12-23 15:52:27 -08:00
Alexander Holliday bd81c49b1e Revert "hotfix: move middleware to after JobQueue initialization" 2024-12-23 15:52:16 -08:00
Alexander Holliday b2e2d08561 Merge pull request #1467 from bluewave-labs/hotfix/be/move-middleware
hotfix: move middleware to after JobQueue initialization
2024-12-23 15:17:41 -08:00
Alex Holliday fdb77bb073 move middleware to after JobQueue initialization 2024-12-23 15:16:52 -08:00
Alexander Holliday 6a469392d3 Merge pull request #1466 from bluewave-labs/hotfix/devops/docker-compose
fix test config docker-compose URL
2024-12-23 15:09:10 -08:00
Alex Holliday be45dba3df fix test config docker-compose URL 2024-12-23 15:08:36 -08:00
Alexander Holliday 5147a685e0 Merge pull request #1462 from bluewave-labs/fix/devops/restart-docker-images
fix: always restart docker images
2024-12-23 13:42:09 -08:00
Alexander Holliday ad2af345e8 Merge pull request #1463 from bluewave-labs/hotfix/fe/theme
neuter -> neutral
2024-12-23 10:42:53 -08:00
Alex Holliday 10d91334d8 neuter -> neutral 2024-12-23 10:42:24 -08:00
Alex Holliday b2b9816b6b always restart docker images 2024-12-23 09:49:01 -08:00
Alexander Holliday ef6e97796f Merge pull request #1460 from bluewave-labs/fix/button-contrast
Fix: group buttons contrast text
2024-12-23 09:46:11 -08:00
Caio Cabral addd707790 neuter to neutral 2024-12-23 12:44:12 -05:00
Alexander Holliday f7c4841094 Merge pull request #1444 from bluewave-labs/fix/fe/uptime-skeleton-and-fallback
fix: add additional not loading flag to fallback conditional rendering, resolves #1426
2024-12-23 09:40:52 -08:00
Alex Holliday 1d038cff39 simplify skeleton loading 2024-12-23 09:36:53 -08:00
Alexander Holliday 4b1b9f2e66 Merge pull request #1461 from bluewave-labs/fix/sidebar
feat: adjustments to sidebar
2024-12-23 07:17:47 -08:00
Caio Cabral 3719644d5c feat: adjustments to sidebar 2024-12-23 09:51:41 -05:00
Caio Cabral c827c4404a ix: group buttons contrast text 2024-12-23 07:32:08 -05:00
Alexander Holliday 8b291ce903 Merge pull request #1458 from bluewave-labs/fix/row-hover
fix: row background color on hover
2024-12-22 22:53:20 -08:00
Alexander Holliday 4cd2ea1442 Merge pull request #1456 from mertssmnoglu/fix/server-old-name
Fix: Old name reference on the Server Side
2024-12-22 18:20:23 -08:00
Alexander Holliday 85a73fd08b Merge pull request #1457 from bluewave-labs/fix/response-chart-color
fix: pie chart colors and Metrics box that was overflowing
2024-12-22 18:16:22 -08:00
Alexander Holliday 0221e5b077 Merge pull request #1443 from bluewave-labs/fix/be/heap-size-and-dependency-bump
fix: add env var to increase node heap size, bump server dependencies
2024-12-22 17:28:01 -08:00
Alexander Holliday e294b6343a Merge branch 'develop' into fix/be/heap-size-and-dependency-bump 2024-12-22 17:27:52 -08:00
Alexander Holliday 639fd425d7 Merge pull request #1442 from bluewave-labs/fix/be/hardware-check-concurrency
fix: hardware-check concurrency issue
2024-12-22 17:24:51 -08:00
Alexander Holliday fbb01cd7af Merge pull request #1454 from renovate-bot/renovate/react-router-monorepo
fix(deps): update react-router monorepo to v6.28.1
2024-12-22 17:22:30 -08:00
Alexander Holliday 3c3194687a Merge pull request #1455 from renovate-bot/renovate/bullmq-5.x
fix(deps): update dependency bullmq to v5.34.4
2024-12-22 17:22:11 -08:00
Caio Cabral e1ea105872 fix: row background color on hover 2024-12-22 20:07:53 -05:00
Caio Cabral 49f675cf02 fix: pie chart colors and Metrics box that was overflowing 2024-12-22 19:52:40 -05:00
Mert Şişmanoğlu e84a7b4a2e fix: Update references from Bluewave Uptime to Checkmate in email templates and authController 2024-12-22 00:15:05 +03:00
Mert Şişmanoğlu 9f04e786e3 chore(openapi): Rename old bluewave uptime to checkmate in Server/openapi.json 2024-12-22 00:08:45 +03:00
Gorkem Cetin (BWL) 213c055fad Update README.md 2024-12-21 13:41:51 -05:00
Gorkem Cetin (BWL) c2b7d1a959 Update README.md 2024-12-21 13:41:35 -05:00
Mend Renovate 5ac77a96f0 fix(deps): update dependency bullmq to v5.34.4 2024-12-21 05:09:28 +00:00
Gorkem Cetin (BWL) 4707bda1b8 Additions & fixes for readme.md 2024-12-20 19:58:44 -05:00
Mend Renovate e431930585 fix(deps): update react-router monorepo to v6.28.1 2024-12-20 22:55:56 +00:00
Alexander Holliday d64aae3ff0 Merge pull request #1453 from bluewave-labs/dependabot/npm_and_yarn/Client/nanoid-3.3.8
chore(deps): bump nanoid from 3.3.7 to 3.3.8 in /Client
2024-12-20 14:55:42 -08:00
Alexander Holliday 6b1f892852 Merge pull request #1451 from bluewave-labs/fix/be/duplicate-success-msg
fix: remove duplicate msg, resolves #1446
2024-12-20 14:55:27 -08:00
Alexander Holliday ca722091b5 Merge pull request #1452 from mertssmnoglu/feat/server-toolchain
Update development toolchain of the `Server/`
2024-12-20 14:55:10 -08:00
Mert Şişmanoğlu d4a08214e8 chore(eslint): Add ecmaVersion and sourceType to 'languageOptions' 2024-12-21 00:56:04 +03:00
dependabot[bot] de25ad7623 chore(deps): bump nanoid from 3.3.7 to 3.3.8 in /Client
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.7 to 3.3.8.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-20 21:09:20 +00:00
Mert Şişmanoğlu a8a36dac1b chore(package): Add new package scripts for linting and formatting
- lint
- lint-fix
- format
- format-check
2024-12-20 23:49:54 +03:00
Mert Şişmanoğlu f9874d33fc chore(eslint): Import ESlint to the /Server
- Exec 'npm init @eslint/config@latest'
- Update configuration according to docs
2024-12-20 23:29:48 +03:00
Alex Holliday a593768c23 remove duplicate msg 2024-12-20 11:48:49 -08:00
Alexander Holliday 021011f83e Merge pull request #1409 from peterpardo/feat/dark-mode-switch
Feat: Add dark mode switch
2024-12-20 10:22:03 -08:00
Alexander Holliday 73855b0cd3 Merge pull request #1445 from renovate-bot/renovate/react-monorepo
chore(deps): update dependency @types/react to v18.3.18
2024-12-20 10:16:04 -08:00
Peter Carl Pardo 0c16a62131 feat: removed unused imports 2024-12-20 23:48:25 +08:00
Peter Carl Pardo b5a125b388 feat: added theme switch and forgot password label in login page 2024-12-20 23:38:55 +08:00
Peter Carl Pardo 3f7d3efe2b feat: removed forgot password label in email and password step 2024-12-20 23:38:16 +08:00
Peter Carl Pardo 9ef1c3572e feat: created ForgotPasswordLabel.jsx component 2024-12-20 23:37:58 +08:00
Peter Carl Pardo 4e85e60735 Merge branch 'develop' into feat/dark-mode-switch 2024-12-20 23:14:17 +08:00
Peter Carl Pardo b97e70f162 feat: moved theme switch stylings 2024-12-20 23:07:36 +08:00
Mend Renovate 65b183a9f2 chore(deps): update dependency @types/react to v18.3.18 2024-12-20 05:43:49 +00:00
Alex Holliday 98199f2e1c Add additional not loading flag to fallback conditional rendering 2024-12-19 12:03:09 -08:00
Alex Holliday 7d5df4b7b8 add env var to increase node heap size, bump server dependencies 2024-12-19 11:55:34 -08:00
Alex Holliday 914217058b use to resolve concurrency issue 2024-12-19 11:52:27 -08:00
Alexander Holliday 4a78adfefd Merge pull request #1441 from bluewave-labs/hotfix/fe/uptime-details
add missing import
2024-12-19 10:19:03 -08:00
Alex Holliday 0afbd527b3 add missing import 2024-12-19 10:18:25 -08:00
Alexander Holliday 4a8d5b73eb Merge pull request #1440 from Anand-Royy/tab-border
Tab bars are displayed upwards when hovered on them #1438
2024-12-19 09:45:28 -08:00
Alexander Holliday 34f3f3ddbf Merge pull request #1434 from bluewave-labs/fix/fe/infra-boxes
fix: fe/infra boxes, resolves #1427
2024-12-19 09:37:44 -08:00
Alex Holliday d42610caaa Merge branch 'develop' into fix/fe/infra-boxes 2024-12-19 09:37:05 -08:00
Alex Holliday 29a12da133 Remove console log 2024-12-19 09:31:21 -08:00
Alexander Holliday b02236c2e2 Merge pull request #1437 from renovate-bot/renovate/mongoose-8.x-lockfile
fix(deps): update dependency mongoose to v8.9.2
2024-12-19 09:29:08 -08:00
Alexander Holliday fe829c4d6a Merge pull request #1433 from bluewave-labs/fix/fe/refactor-config-box
fix: fe/refactor config box
2024-12-19 09:28:26 -08:00
Alexander Holliday a16ede06c8 Merge pull request #1435 from bluewave-labs/fix/fe/refactor-icon-box
fix: fe/refactor icon box
2024-12-19 09:28:08 -08:00
Alexander Holliday e9e1f04115 Merge pull request #1439 from renovate-bot/renovate/material-ui-monorepo
fix(deps): update material-ui monorepo to v7.23.3
2024-12-19 09:27:50 -08:00
Alexander Holliday 3fb70353da Merge pull request #1436 from bluewave-labs/fix/fe/protected-route-proptype
fix: protected route proptype
2024-12-19 09:27:33 -08:00
Anand Roy 4da2683404 fixed the tab border 2024-12-19 16:12:07 +00:00
Mend Renovate 77893a5fe3 fix(deps): update material-ui monorepo to v7.23.3 2024-12-19 15:48:38 +00:00
Peter Carl Pardo 76dc4ebb0c feat: removed import for easings functionality 2024-12-19 21:18:23 +08:00
Mend Renovate 862a8f05b7 fix(deps): update dependency mongoose to v8.9.2 2024-12-19 12:55:15 +00:00
Alex Holliday b70fc7e9c6 Fix protected route proptype 2024-12-18 14:26:15 -08:00
Alex Holliday 21b876a723 Use IconBox consistently 2024-12-18 14:11:11 -08:00
Alex Holliday 42f4cc8ddd Create reusable IconBox 2024-12-18 14:11:03 -08:00
Alex Holliday eebf438c13 Add JSdoc and proptypes 2024-12-18 13:46:51 -08:00
Alex Holliday e37413771e Use StatBox consistently 2024-12-18 13:45:02 -08:00
Alex Holliday aee3e6a604 refactor StatBox to reusable component 2024-12-18 13:44:48 -08:00
Alex Holliday b55aee8511 remove old config boxes 2024-12-18 12:20:18 -08:00
Alex Holliday c47ec6ce8b refactor create maintenance to use new ConfigBox 2024-12-18 12:20:09 -08:00
Alex Holliday bd5bc2617c Add a config box component 2024-12-18 12:19:21 -08:00
Alexander Holliday e5a7860ff0 Merge pull request #1414 from Anand-Royy/radio
Add a new uptime monitor > Radio buttons: unselected radio buttons are hard to see #1404 #1412
2024-12-18 10:23:10 -08:00
Alexander Holliday 6a1ebbccfe Merge pull request #1432 from Anand-Royy/reset-pass
Reset password link should be present in all pages #1403
2024-12-18 10:18:11 -08:00
Alexander Holliday bbb791579b Merge pull request #1431 from bluewave-labs/1378-settings-page-should-have-left-and-right-dividers
- Add divider for setting
2024-12-18 09:59:47 -08:00
Anand Roy 7f7423aed8 reset password link added 2024-12-18 17:46:44 +00:00
Shemy Gan 6e29e37241 - Add divider for setting 2024-12-18 10:42:41 -05:00
Alexander Holliday ff6c890eb0 Merge pull request #1429 from renovate-bot/renovate/bullmq-5.x
fix(deps): update dependency bullmq to v5.34.3
2024-12-18 06:39:46 -08:00
Mend Renovate 3cc7cf8dea fix(deps): update dependency bullmq to v5.34.3 2024-12-18 13:33:35 +00:00
Peter Carl Pardo 7c5c3507d8 feat: removed useEffect in toggling theme 2024-12-18 18:57:38 +08:00
Alexander Holliday d9d96031a4 Merge pull request #1424 from bluewave-labs/feat/fe/team-empty-view
feat: fe/team empty view, resolves #1400
2024-12-17 16:18:07 -08:00
Alexander Holliday 1d4f67a899 Merge pull request #1425 from bluewave-labs/fix/fe/remove-advanced-settings
fix: remove advanced status page, resolves #1379
2024-12-17 16:17:47 -08:00
Alex Holliday 1bc6717e72 remove advanced status page 2024-12-17 14:04:42 -08:00
Alex Holliday f880c2623c Supply empty message to basic table 2024-12-17 13:51:03 -08:00
Alex Holliday 1e6b339483 Add empty message to BasicTable component 2024-12-17 13:50:55 -08:00
Alexander Holliday 4ff4c95951 Merge pull request #1423 from bluewave-labs/hotfix/route-import
hotfix: login import
2024-12-17 13:40:33 -08:00
Alex Holliday f631fb16ac fix login import 2024-12-17 13:39:55 -08:00
Alexander Holliday d22ca5098a Merge pull request #1422 from bluewave-labs/fix/fe/login-route
fix: fe/login route
2024-12-17 13:11:55 -08:00
Alex Holliday 1f823fc672 remove old login page 2024-12-17 13:10:40 -08:00
Alex Holliday a507e1282e Fix links in login page 2024-12-17 13:10:28 -08:00
Alexander Holliday a194732152 Merge pull request #1411 from sha-linimoorthy/issue_#1402
Changed the avatar color to checkmate blue color
2024-12-17 10:05:40 -08:00
Alexander Holliday 7d1e7ccb94 Merge pull request #1419 from bluewave-labs/fix/fe/password-boot
fix: incorrect password on password change causes user to be logged out, resolves #1384
2024-12-17 10:03:18 -08:00
Alex Holliday a42e280e49 update comments 2024-12-17 10:02:56 -08:00
Peter Carl Pardo acefedd6e7 feat: add attribution for theme switch 2024-12-17 23:57:12 +08:00
Alexander Holliday ab2dec2a4f Merge pull request #1375 from bluewave-labs/fix/be/invite-route
fix: invite route
2024-12-17 07:30:15 -08:00
Alexander Holliday 5ebd6fd79a Merge pull request #1417 from bluewave-labs/fix/infra-monitor-empty-view
fix: infrasctrucutre details empty view message, resolves #1401
2024-12-17 07:29:48 -08:00
Alexander Holliday 2a3feb1c69 Merge pull request #1418 from bluewave-labs/fix/devops/dockerignore
fix: update dockeringore
2024-12-17 07:29:17 -08:00
Alexander Holliday 063ec5ffe6 Merge pull request #1420 from renovate-bot/renovate/material-ui-monorepo
fix(deps): update material-ui monorepo
2024-12-17 07:29:03 -08:00
Mend Renovate a9ed2158e9 fix(deps): update material-ui monorepo 2024-12-17 15:28:45 +00:00
Alexander Holliday d2daf54eb8 Merge pull request #1383 from bluewave-labs/fix/fe/monitor-pause-toast
fix: add pause toast to PageSpeed and Uptime configure pages, resolves #1380
2024-12-17 07:28:35 -08:00
Alexander Holliday f1b6e6fcc8 Merge pull request #1376 from bluewave-labs/fix/fe/sidebar-logo-navigation
fix: logo navigation, resolves #1320
2024-12-17 07:27:57 -08:00
Shalini 2d27eed9ba Used theme for the avatar BG instead of hard coding 2024-12-17 10:38:20 +05:30
Alex Holliday df11a457f1 401 -> 403 2024-12-16 18:20:41 -08:00
Alex Holliday d94ba5c524 update dockeringore 2024-12-16 17:58:58 -08:00
Alex Holliday 0aa46aa2c2 fix empty view message 2024-12-16 17:55:45 -08:00
Alexander Holliday d4a6df5df3 Merge pull request #1415 from renovate-bot/renovate/mongoose-8.x-lockfile
fix(deps): update dependency mongoose to v8.9.1
2024-12-16 16:53:46 -08:00
Mend Renovate 0022d839df fix(deps): update dependency mongoose to v8.9.1 2024-12-16 23:54:02 +00:00
Anand Roy dd5527931d modified radio button visibility 2024-12-16 18:59:29 +00:00
Alexander Holliday 1364004702 Merge pull request #1345 from bluewave-labs/1321-remove-continue-with-email-page-which-is-an-unnecessary-step
- Remove the login landing page
2024-12-16 10:49:15 -08:00
Alex Holliday 3c725814cf Merge branch 'develop' into 1321-remove-continue-with-email-page-which-is-an-unnecessary-step 2024-12-17 02:47:44 +08:00
Alexander Holliday abc88aa6c6 Merge pull request #1413 from renovate-bot/renovate/react-monorepo
chore(deps): update dependency @types/react to v18.3.17
2024-12-16 09:36:56 -08:00
Peter Carl Pardo 663f2334d1 feat: added dark mode functionality 2024-12-17 01:24:15 +08:00
Mend Renovate 69ef798f60 chore(deps): update dependency @types/react to v18.3.17 2024-12-16 16:57:03 +00:00
Peter Carl Pardo 0e8b82be97 Merge branch 'develop' into feat/dark-mode-switch 2024-12-16 22:35:16 +08:00
Gorkem Cetin (BWL) 37f74271aa 2500 * 2024-12-16 09:24:39 -05:00
Shalini M e30b64bed3 Changed the avatar color to checkmate blue color 2024-12-16 17:08:27 +05:30
Peter Carl Pardo e6faf96984 cleanup 2024-12-16 14:28:58 +08:00
Peter Carl Pardo c565cc7362 Merge branch 'develop' into feat/dark-mode-switch 2024-12-16 14:18:25 +08:00
Peter Carl Pardo cdcefb1106 feat: add theme switch in sidebar 2024-12-16 14:16:58 +08:00
Peter Carl Pardo 3ab8b1241e feat: Add theme switch in login page 2024-12-16 14:05:56 +08:00
Gorkem Cetin (BWL) 265af61100 Add a warning line in pull_request_template.md 2024-12-15 15:09:33 -05:00
Gorkem Cetin (BWL) 7cbeb75186 Update README.md 2024-12-15 14:45:57 -05:00
Peter Carl Pardo a97b2f0887 feat: cleanup 2024-12-15 23:52:07 +08:00
Peter Carl Pardo efa106f732 feat: add comment for temporary theme logic 2024-12-15 23:51:53 +08:00
Peter Carl Pardo dd488fecb4 feat: cleanup 2024-12-15 23:50:48 +08:00
Peter Carl Pardo 80dee67c08 Merge branch 'develop' into feat/dark-mode-switch 2024-12-15 23:49:05 +08:00
Peter Carl Pardo 6d6a13a833 feat: fix sun beams icon bug 2024-12-15 23:46:16 +08:00
Peter Carl Pardo 55225ad221 feat: initial dark theme switch component 2024-12-15 23:11:50 +08:00
Alexander Holliday 592cc0d615 Merge pull request #1369 from bluewave-labs/refactor/routes
Refactor/routes
2024-12-14 23:06:30 -08:00
Alexander Holliday 3c960d93db Merge pull request #1385 from renovate-bot/renovate/nodemon-3.x
chore(deps): update dependency nodemon to v3.1.9
2024-12-14 23:03:26 -08:00
Alexander Holliday 3c1601e75c Merge pull request #1389 from renovate-bot/renovate/bullmq-5.x
Update dependency bullmq to v5.34.2
2024-12-14 23:03:07 -08:00
Alexander Holliday 066ad535c5 Merge pull request #1391 from renovate-bot/renovate/mongoose-8.x-lockfile
Update dependency mongoose to v8.9.0
2024-12-14 23:02:50 -08:00
Alexander Holliday 00636d862b Merge pull request #1393 from bluewave-labs/dependabot/npm_and_yarn/Server/nanoid-3.3.8
chore(deps): bump nanoid from 3.3.7 to 3.3.8 in /Server
2024-12-14 23:02:32 -08:00
Alexander Holliday dd5b0e2db4 Merge pull request #1394 from mertssmnoglu/chore/renovate-add-label
chore(renovate): Auto label assign on Renovate PRs
2024-12-14 23:02:06 -08:00
Gorkem Cetin (BWL) a0289d1893 Update README.md 2024-12-14 20:55:51 -05:00
Gorkem Cetin (BWL) cd2076ff16 Update: stress testing for 1000+ servers at the same time 2024-12-14 20:50:03 -05:00
Mend Renovate 1b0e7ddd50 fix(deps): update dependency bullmq to v5.34.2 2024-12-14 21:10:32 +00:00
Gorkem Cetin (BWL) c6ce627495 Update README.md 2024-12-14 11:53:53 -05:00
Gorkem Cetin (BWL) 425358e1e3 Thank you for 2000 stars! 2024-12-14 10:12:22 -05:00
Gorkem Cetin (BWL) 09644dfcfe Add links to 2 documents for contributors 2024-12-13 22:45:50 -05:00
Gorkem Cetin (BWL) ab0d1ab469 Update README.md 2024-12-13 21:04:21 -05:00
Gorkem Cetin (BWL) 528bc0b1e0 Update pull_request_template.md 2024-12-13 19:04:29 -05:00
Caio Cabral 7d7c616a73 fix: function not called 2024-12-13 17:56:37 -05:00
Mert Şişmanoğlu 2628f2ae32 chore(renovate): Add 'labels' to renovate.json for adding 'dependencies' label on automated PRs 2024-12-14 00:27:26 +03:00
dependabot[bot] 946e74aeab chore(deps): bump nanoid from 3.3.7 to 3.3.8 in /Server
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.7 to 3.3.8.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-13 21:15:02 +00:00
Gorkem Cetin (BWL) 2e970f6f84 Update README.md 2024-12-13 15:18:23 -05:00
Gorkem Cetin (BWL) 0cfb08d03a Mention good-first-issue 2024-12-13 13:20:33 -05:00
Mend Renovate fb127f5ba2 fix(deps): update dependency mongoose to v8.9.0 2024-12-13 17:31:01 +00:00
Gorkem Cetin (BWL) 7bef37f8c8 Add coolify option 2024-12-13 11:11:40 -05:00
Shemy Gan c72334253d - Refactor out/Rename the StepOne and StepTwo components 2024-12-13 10:50:36 -05:00
Mend Renovate b2f8961bac chore(deps): update dependency nodemon to v3.1.9 2024-12-13 13:19:47 +00:00
Alex Holliday 4b47d43cd1 Add pause toast to PageSpeed and Uptime configure pages 2024-12-13 13:12:38 +08:00
Gorkem Cetin (BWL) 1cf7df1ac6 Merge pull request #1382 from bluewave-labs/gorkem-bwl-patch-1
Fix wording
2024-12-12 23:53:09 -05:00
Gorkem Cetin (BWL) abfd279707 Fix wording 2024-12-12 23:52:35 -05:00
Alex Holliday 35dcf19169 clicking on logo redirects to 2024-12-13 11:52:49 +08:00
Alex Holliday daf5ccb342 Fix invite route 2024-12-13 11:45:41 +08:00
Alexander Holliday fd1232d72e Merge pull request #1374 from bluewave-labs/fix/monitor-fallback-link
fix: fallback link
2024-12-12 19:20:50 -08:00
Alex Holliday b651d09769 Fix fallback link 2024-12-13 11:20:09 +08:00
Alexander Holliday ba5f58683e Merge pull request #1344 from peterpardo/fix/update-average-response-time-chart-color
fix: update red palette colors
2024-12-12 17:54:38 -08:00
Gorkem Cetin (BWL) ab0c509c92 Update README.md 2024-12-12 20:23:21 -05:00
Gorkem Cetin (BWL) 265f2a7e4a Update README.md 2024-12-12 20:17:18 -05:00
Alexander Holliday 1f823c120b Merge pull request #1371 from renovate-bot/renovate/material-ui-monorepo
fix(deps): update material-ui monorepo to v7.23.2
2024-12-12 15:25:40 -08:00
Alexander Holliday 3aeb1cea1c Merge pull request #1372 from renovate-bot/renovate/recharts-2.x
fix(deps): update dependency recharts to v2.15.0
2024-12-12 15:25:28 -08:00
Mend Renovate fd89cbca1d fix(deps): update dependency recharts to v2.15.0 2024-12-12 22:43:46 +00:00
Caio Cabral a8a8664fc6 refactor: simplify protected route component 2024-12-12 14:43:57 -05:00
Mend Renovate 1aa05eb438 fix(deps): update material-ui monorepo to v7.23.2 2024-12-12 19:05:54 +00:00
Gorkem Cetin (BWL) 4f2a1b7342 Update README.md 2024-12-12 14:03:26 -05:00
Peter Carl Pardo 017eab53ff Merge branch 'develop' into fix/update-average-response-time-chart-color 2024-12-13 00:47:59 +08:00
Peter Carl Pardo 1a856ed463 fix: Update chart color for "Poor" category 2024-12-13 00:47:09 +08:00
Caio Marcellus Cabral 476595e891 Merge pull request #1370 from bluewave-labs/hotFix/monitors-uptime
hotfix: imports
2024-12-12 10:43:57 -05:00
Caio Cabral dc46a0c2b2 hotfix: imports 2024-12-12 10:23:36 -05:00
Peter Carl Pardo c3298147b6 Revert "fix: update red palette colors"
This reverts commit 79dd781386.
2024-12-12 22:39:42 +08:00
Caio Cabral ee4ef15874 resolving conflicts 2024-12-12 09:20:24 -05:00
Caio Cabral 788ffc92c8 resolving conflicts 2024-12-12 09:15:39 -05:00
Caio Cabral cce1ed610c chore: delete console 2024-12-12 09:11:08 -05:00
Caio Cabral 910080820b refactor: removed withAdminProps HOC and changed for custom hook reducing component wrapping 2024-12-12 09:07:11 -05:00
Caio Cabral a70e96f1f3 refactor: avoid repetition of the protected routes component 2024-12-12 08:24:10 -05:00
Alexander Holliday 820c6780dd Merge pull request #1367 from tom-doerr/patch-1
Update README.md
2024-12-11 23:10:35 -08:00
Tom Dörr 7d33ac11b5 Update README.md 2024-12-12 06:33:32 +01:00
Gorkem Cetin (BWL) 52234531db Update pull_request_template.md 2024-12-12 00:17:25 -05:00
Gorkem Cetin (BWL) 2145be5160 Minor update to pull_request_template.md 2024-12-12 00:13:10 -05:00
Caio Cabral 66dc6dbc1a refactor: extracting routes to own component 2024-12-12 00:09:41 -05:00
Alexander Holliday 79c4cfc2e8 Merge pull request #1310 from bluewave-labs/fix/fe/create-infra-refactor
fix: fe/create infra refactor
2024-12-11 20:59:32 -08:00
Alexander Holliday d46e6c7cba Merge pull request #1338 from bluewave-labs/fix/fe/monitors-rename
fix:  Monitors -> Uptime resovles #1333
2024-12-11 20:59:08 -08:00
Alexander Holliday a7ed349af4 Merge pull request #1364 from mertssmnoglu/demo-url-change
fix: update demo server URLs to 'checkmate-demo.bluewavelabs.ca'
2024-12-11 20:58:39 -08:00
Alex Holliday e33091f182 remove 2024-12-12 12:42:32 +08:00
Alex Holliday 3840b3ec2d remove log statement 2024-12-12 12:38:43 +08:00
Alexander Holliday 33ed54191c Merge branch 'develop' into fix/fe/monitors-rename 2024-12-11 20:35:52 -08:00
Alexander Holliday 5579e8a339 Merge pull request #1303 from bluewave-labs/fix/fe/create-pagespeed-refactor
fix: Refactor Create PageSpeed
2024-12-11 20:34:07 -08:00
Alexander Holliday 6cdd839d6a Merge branch 'develop' into fix/fe/create-pagespeed-refactor 2024-12-11 20:33:57 -08:00
Alex Holliday fff8528092 Merge branch 'develop' into fix/fe/monitors-rename 2024-12-12 12:27:10 +08:00
Alex Holliday f57b9a0fa1 remove daterange from dependency array 2024-12-12 12:26:24 +08:00
Alexander Holliday c251f5cba9 Merge pull request #1362 from INDHU1402/fix/fe/update-text
Changed text from "Usage" to "usage" in UI
2024-12-11 20:19:53 -08:00
Alexander Holliday 49209e8cc5 Merge pull request #1366 from bluewave-labs/refactor/rever-infrastructure
Refactor/rever infrastructure
2024-12-11 19:27:11 -08:00
Caio Cabral 24d8d3e8d5 Merge branch 'develop' of https://github.com/bluewave-labs/bluewave-uptime into refactor/rever-infrastructure 2024-12-11 22:08:42 -05:00
Alexander Holliday 48d46253b3 Merge pull request #1311 from bluewave-labs/fix/be/delete-hardware-checks
add deleteHardwareChecksByMonitorId
2024-12-11 18:49:25 -08:00
Caio Cabral 5910bdb9c5 feat: adding same sx to buttons on monitors 2024-12-11 21:18:09 -05:00
Alexander Holliday 903b98b2e7 Merge pull request #1365 from bluewave-labs/refactor/rever-infrastructure
refactor: reverting unused function in infrastructure page
2024-12-11 18:08:37 -08:00
Caio Cabral 8bf254ee49 refactor: reverting unused function in infrastructure page 2024-12-11 21:01:27 -05:00
Alexander Holliday 80e9fa478b Merge pull request #1363 from renovate-bot/renovate/material-ui-monorepo
fix(deps): update material-ui monorepo
2024-12-11 17:57:27 -08:00
Alexander Holliday 6379e614ee Merge pull request #1354 from bluewave-labs/fix/fe/update-pause-count
fix: FE monitor pause count resolves #1325
2024-12-11 17:56:23 -08:00
Alexander Holliday bd2b8488ed Merge pull request #1359 from renovate-bot/renovate/react-monorepo
chore(deps): update dependency @types/react-dom to v18.3.5
2024-12-11 17:42:02 -08:00
Alexander Holliday 04907bcb19 Merge pull request #1357 from bluewave-labs/fix/fe/remove-console
Remove console log statement
2024-12-11 17:41:45 -08:00
Mert Şişmanoğlu b41fe1e70e fix: update demo server URLs to 'checkmate-demo.bluewavelabs.ca' 2024-12-12 00:31:27 +03:00
Shemy Gan 76bfb7717a - Remove onBack from stepOne
- Set step to 0 index
2024-12-11 11:10:19 -05:00
Shemy Gan 7a57d2734a Merge branch 'develop' into 1321-remove-continue-with-email-page-which-is-an-unnecessary-step 2024-12-11 11:04:35 -05:00
Shemy Gan ba8cc04f2d - Remove unused import 2024-12-11 11:03:58 -05:00
Mend Renovate 99c1543825 fix(deps): update material-ui monorepo 2024-12-11 15:39:05 +00:00
Indhu c11726c920 Changed text from Usage --> usage in UI 2024-12-11 15:03:43 +05:30
Mend Renovate bd88079876 chore(deps): update dependency @types/react-dom to v18.3.5 2024-12-11 03:11:43 +00:00
Alexander Holliday 76d0d1b486 Merge pull request #1358 from renovate-bot/renovate/react-monorepo
chore(deps): update dependency @types/react to v18.3.16
2024-12-10 19:11:13 -08:00
Mend Renovate 8bf9c13527 chore(deps): update dependency @types/react to v18.3.16 2024-12-11 03:03:39 +00:00
Alexander Holliday 567ff071ed Merge pull request #1355 from renovate-bot/renovate/reduxjs-toolkit-2.x
fix(deps): update dependency @reduxjs/toolkit to v2.5.0
2024-12-10 19:02:44 -08:00
Alex Holliday d993277463 Remove console log statement 2024-12-11 11:00:50 +08:00
Mend Renovate d17dd8f8b6 fix(deps): update dependency @reduxjs/toolkit to v2.5.0 2024-12-11 02:52:14 +00:00
Alexander Holliday d93b4551b4 Merge pull request #1336 from bluewave-labs/fix/fe/infra-button-vertical-spacing
Fix/fe/infra button vertical spacing, resolves #1332, resolves #1334
2024-12-10 18:51:40 -08:00
Alex Holliday d5acfd9e9e Add update trigger for pause state 2024-12-11 10:46:54 +08:00
Alexander Holliday 3350485d5e Merge pull request #1339 from renovate-bot/renovate/bullmq-5.x
fix(deps): update dependency bullmq to v5.34.0
2024-12-10 18:29:36 -08:00
Alexander Holliday ed075e5f05 Merge pull request #1341 from peterpardo/fix/adjust-search-bar-height
Fix: Adjust Search Bar height
2024-12-10 18:29:22 -08:00
Alexander Holliday f32f994036 Merge pull request #1347 from bluewave-labs/1324-monitor-page-pending-label-should-be-gray-instead-of-yellow-which-is-same-as-paused
- Change to grey  color for pending status
2024-12-10 17:52:50 -08:00
Alexander Holliday 796a66a43c Merge pull request #1348 from bluewave-labs/1231-remove-unusedunimplemented-configuration
- Remove text from configuration
2024-12-10 17:52:06 -08:00
Alexander Holliday 538c5c8d02 Merge pull request #1352 from renovate-bot/renovate/react-monorepo
chore(deps): update dependency @types/react-dom to v18.3.4
2024-12-10 17:51:41 -08:00
Alexander Holliday 0182c37fc7 Merge pull request #1353 from renovate-bot/renovate/react-redux-9.x
fix(deps): update dependency react-redux to v9.2.0
2024-12-10 17:51:25 -08:00
Mend Renovate a78d595c9f fix(deps): update dependency react-redux to v9.2.0 2024-12-11 01:16:49 +00:00
Mend Renovate 4f45be6dfb chore(deps): update dependency @types/react-dom to v18.3.4 2024-12-11 01:16:43 +00:00
Alexander Holliday 6499f70e20 Merge pull request #1349 from renovate-bot/renovate/c8-10.x
chore(deps): update dependency c8 to v10.1.3
2024-12-10 15:12:56 -08:00
Alexander Holliday ab0f934ac6 Merge pull request #1351 from renovate-bot/renovate/react-monorepo
chore(deps): update dependency @types/react to v18.3.15
2024-12-10 15:12:45 -08:00
Alexander Holliday cad881ad10 Merge pull request #1343 from bluewave-labs/1330-remove-disabled-options-in-monitors-page-speed-sections-as-they-are-not-ready-yet
- Remove disabled notifications
2024-12-10 15:09:24 -08:00
Alexander Holliday 1fe5c8aec7 Merge pull request #1329 from bluewave-labs/gorkem-bwl-patch-1
Editting -> Editing
2024-12-10 15:08:53 -08:00
Mend Renovate d52844eb53 chore(deps): update dependency @types/react to v18.3.15 2024-12-10 22:08:21 +00:00
Mend Renovate c04ce25eb0 chore(deps): update dependency c8 to v10.1.3 2024-12-10 20:00:59 +00:00
Shemy Gan 9c48ffe977 - Remove text from configuration 2024-12-10 14:53:07 -05:00
Shemy Gan 1d56c5b674 - Change to grey color for pending status 2024-12-10 14:41:06 -05:00
Shemy Gan cb7f596b0d - Remove the login landing page
- Remove the back button as it is not needed anymore
- Adjust layout due to the removal of backbutton and remove the propTypes requirement for onBack function
2024-12-10 11:21:59 -05:00
Mend Renovate 5e22401df4 fix(deps): update dependency bullmq to v5.34.0 2024-12-10 16:08:52 +00:00
Peter Carl Pardo 79dd781386 fix: update red palette colors 2024-12-11 00:03:36 +08:00
Shemy Gan b02044ae7f - Remove disabled notifications 2024-12-10 10:16:04 -05:00
Peter Carl Pardo f26c0a0575 fix: removing padding Y of search bar 2024-12-10 20:45:45 +08:00
Gorkem Cetin (BWL) 151500def6 Add screenshots 2024-12-10 01:31:40 -05:00
Alex Holliday 9c793da0f0 Monitors -> Uptime 2024-12-10 14:23:46 +08:00
Alex Holliday 184c0093bd Add monitor count to PageSpeed page 2024-12-10 14:00:45 +08:00
Alex Holliday 1c655dd35a Add matching header to infrastructure page 2024-12-10 14:00:24 +08:00
Gorkem Cetin (BWL) 92494d58f6 Editting -> Editing 2024-12-10 00:23:00 -05:00
Alexander Holliday ad9bda444b Merge pull request #1315 from RishistavRay/feat/aside-viewport-height
Fix Sidebar height to occupy viewport height
2024-12-09 19:45:03 -08:00
Alex Holliday 2226a2f1cf Update jsdocs 2024-12-10 11:32:06 +08:00
Alex Holliday 06cb22e4d8 Use name instead of extra param 2024-12-10 11:30:38 +08:00
Alex Holliday f64e4d04ef Add checkbox name and fieldname props to CustomThreshold 2024-12-10 11:30:25 +08:00
Alex Holliday de149503ae Expose name prop in Checkbox component 2024-12-10 11:29:57 +08:00
Alex Holliday b306a4ac7b Add name to all inputs, use event.target.name instead of passing extra variable 2024-12-10 11:17:17 +08:00
Alexander Holliday 49e57c5c9c Merge pull request #1312 from renovate-bot/renovate/bullmq-5.x
fix(deps): update dependency bullmq to v5.33.0
2024-12-09 16:03:09 -08:00
Alexander Holliday 7757216d5d Merge pull request #1313 from renovate-bot/renovate/emotion-monorepo
fix(deps): update emotion monorepo to v11.14.0
2024-12-09 16:02:56 -08:00
Alexander Holliday 05d87a9339 Merge pull request #1314 from renovate-bot/renovate/react-monorepo
chore(deps): update dependency @types/react-dom to v18.3.3
2024-12-09 16:02:42 -08:00
Alexander Holliday 912b9f102c Merge pull request #1288 from saksham-malhotra-27/feat-design-1275-Current_password_BG_color
feat #1275: design corrected  : Current Password Field BG color
2024-12-09 16:02:18 -08:00
Alexander Holliday 0a72be7965 Merge pull request #1285 from Shaikh-Mudassir/Mudass
Added a gap between label and inputs #1270
2024-12-09 16:01:47 -08:00
Rishistav 1622e48fc0 Fix Sidebar height to occupy viewport height 2024-12-09 16:55:11 -05:00
Gorkem Cetin (BWL) 97b565a6ad Update README.md 2024-12-09 16:20:18 -05:00
Mend Renovate dc81a0e51b chore(deps): update dependency @types/react-dom to v18.3.3 2024-12-09 20:38:33 +00:00
Mend Renovate d649c53249 fix(deps): update emotion monorepo to v11.14.0 2024-12-09 10:06:34 +00:00
Mend Renovate 7e9383d0eb fix(deps): update dependency bullmq to v5.33.0 2024-12-09 10:06:23 +00:00
Alex Holliday f42bdddb7a add deleteHardwareChecksByMonitorId 2024-12-09 14:01:10 +08:00
Alex Holliday 3e21eb0a12 refactor create infrastructure 2024-12-09 13:35:48 +08:00
Alex Holliday dd487c714f simplify custom threshold 2024-12-09 13:35:21 +08:00
Alex Holliday 9b76bd566a Update monitor and infrastrucutre validation to allow for port numbers 2024-12-09 13:35:00 +08:00
Alexander Holliday 1102d77bfa Merge pull request #1301 from bluewave-labs/fix/fe/create-infra-breadcrumbs
fix: links in create infra monitor page, resolves #1294
2024-12-08 17:44:07 -08:00
Alexander Holliday 412518390f Merge pull request #1309 from renovate-bot/renovate/bullmq-5.x
fix(deps): update dependency bullmq to v5.32.0
2024-12-08 17:42:32 -08:00
Mend Renovate d970ecb3a5 fix(deps): update dependency bullmq to v5.32.0 2024-12-08 18:01:50 +00:00
Alexander Holliday 02fd9d9f0b Merge pull request #1299 from renovate-bot/renovate/react-monorepo
chore(deps): update react monorepo
2024-12-07 17:41:09 -08:00
Alexander Holliday 754cdc53bd Merge pull request #1293 from gauravnadkarni/fix/alignment-required-for-jwt-section
fixed issue with the alignment in JWT section
2024-12-07 17:40:52 -08:00
Alexander Holliday cfcd35b282 Merge pull request #1300 from renovate-bot/renovate/bullmq-5.x
fix(deps): update dependency bullmq to v5.31.2
2024-12-07 17:29:15 -08:00
Shaikh Mohd Mudassir Hussain 9e9d561ea0 Comments were removed and spacing was added to all the stacks issue #1270 2024-12-06 01:47:47 -07:00
Alex Holliday f148e2d9ad Refactor Create PageSpeed 2024-12-06 14:25:11 +08:00
Alex Holliday b4cce16931 fix links in create infra monitor page 2024-12-06 13:18:59 +08:00
Mend Renovate 707a13c3c5 fix(deps): update dependency bullmq to v5.31.2 2024-12-06 05:01:27 +00:00
Mend Renovate a912b6752c chore(deps): update react monorepo 2024-12-06 05:01:19 +00:00
Gorkem Cetin (BWL) 958904e6d5 Update README.md 2024-12-05 22:40:40 -05:00
Alexander Holliday 0c8171ae24 Merge pull request #1297 from renovate-bot/renovate/express-4.x-lockfile
fix(deps): update dependency express to v4.21.2
2024-12-05 18:48:01 -08:00
Alexander Holliday af77c7a84b Merge pull request #1298 from renovate-bot/renovate/mongoose-8.x-lockfile
fix(deps): update dependency mongoose to v8.8.4
2024-12-05 18:47:46 -08:00
Mend Renovate a1ee49e718 fix(deps): update dependency mongoose to v8.8.4 2024-12-06 02:00:30 +00:00
Mend Renovate 6f781b9456 fix(deps): update dependency express to v4.21.2 2024-12-06 02:00:22 +00:00
Alexander Holliday 6fd1c0d465 Merge pull request #1296 from renovate-bot/renovate/react-monorepo
chore(deps): update dependency eslint-plugin-react-hooks to v5.1.0
2024-12-05 17:59:36 -08:00
Alexander Holliday 59f16bf76d Merge pull request #1295 from renovate-bot/renovate/material-ui-monorepo
fix(deps): update material-ui monorepo to v7.23.1
2024-12-05 17:59:19 -08:00
Mend Renovate 471e4fcee9 chore(deps): update dependency eslint-plugin-react-hooks to v5.1.0 2024-12-05 18:44:50 +00:00
Mend Renovate b034288c7f fix(deps): update material-ui monorepo to v7.23.1 2024-12-05 18:44:43 +00:00
Gaurav 996dc36986 added a space before proptype 2024-12-05 17:46:55 +05:30
Gaurav d188efa9ee fixed issue with the alignment in JWT section 2024-12-05 17:29:12 +05:30
Alexander Holliday f4c62dd65c Merge pull request #1292 from bluewave-labs/fix/devops/production
fix: Fix API url
2024-12-04 18:59:22 -08:00
Alex Holliday 5478f64b69 Fix API url 2024-12-05 10:24:50 +08:00
Saksham 09a704775a feat: design corrected #1275 : Current Password Field BG color:updated-removed comments 2024-12-05 07:30:28 +05:30
Saksham e19b9858a1 feat: design corrected #1275 : Current Password Field BG color 2024-12-05 07:20:38 +05:30
Shaikh Mohd Mudassir Hussain ecd1282c4d Added a gap between label and inputs #1270 2024-12-04 15:27:02 -07:00
119 changed files with 4827 additions and 3858 deletions

No files matched your search

+1 -1
View File
@@ -1,3 +1,3 @@
Client/node_modules/
Server/node_modules/
*.env
Server/*.env
@@ -1,6 +1,8 @@
**(when sending your PR, only remove this line. Do not remove any lines below. Check all the items before sending your PR)**
## Describe your changes
Provide a brief description of the changes you’ve made and their purpose.
Briefly describe the changes you made and their purpose.
## Issue number
@@ -8,7 +10,13 @@ Mention the issue number(s) this PR addresses (e.g., #123).
## Please ensure all items are checked off before requesting a review:
- [ ] I have performed a self-review of my code.
- [ ] I deployed the application locally.
- [ ] I have performed a self-review and testing of my code.
- [ ] I have included the issue # in the PR.
- [ ] I have labelled the PR correctly.
- [ ] My PR is granular and targeted to one specific feature only.
- [ ] The issue I am working on is assigned to me.
- [ ] I didn't use any hardcoded values (otherwise it will not scale, and will make it difficult to maintain consistency across the application).
- [ ] I made sure font sizes, color choices etc are all referenced from the theme.
- [ ] My PR is granular and targeted to one specific feature.
- [ ] I took a screenshot or a video and attached to this PR if there is a UI change.
+152 -141
View File
@@ -11,24 +11,24 @@
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@fontsource/roboto": "^5.0.13",
"@mui/icons-material": "6.1.10",
"@mui/lab": "6.0.0-beta.18",
"@mui/material": "6.1.10",
"@mui/icons-material": "6.2.1",
"@mui/lab": "6.0.0-beta.20",
"@mui/material": "6.2.1",
"@mui/x-charts": "^7.5.1",
"@mui/x-data-grid": "7.23.0",
"@mui/x-date-pickers": "7.23.0",
"@reduxjs/toolkit": "2.4.0",
"@mui/x-data-grid": "7.23.3",
"@mui/x-date-pickers": "7.23.3",
"@reduxjs/toolkit": "2.5.0",
"axios": "^1.7.4",
"dayjs": "1.11.13",
"joi": "17.13.3",
"jwt-decode": "^4.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "9.1.2",
"react-redux": "9.2.0",
"react-router": "^6.23.0",
"react-router-dom": "^6.23.1",
"react-toastify": "^10.0.5",
"recharts": "2.14.1",
"recharts": "2.15.0",
"redux-persist": "6.0.0",
"vite-plugin-svgr": "^4.2.0"
},
@@ -352,9 +352,9 @@
}
},
"node_modules/@emotion/cache": {
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.5.tgz",
"integrity": "sha512-Z3xbtJ+UcK76eWkagZ1onvn/wAVb1GOMuR15s30Fm2wrMgC7jzpnO2JZXr4eujTTqoQFUrZIw/rT0c6Zzjca1g==",
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
"integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
"license": "MIT",
"dependencies": {
"@emotion/memoize": "^0.9.0",
@@ -386,16 +386,16 @@
"license": "MIT"
},
"node_modules/@emotion/react": {
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.5.tgz",
"integrity": "sha512-6zeCUxUH+EPF1s+YF/2hPVODeV/7V07YU5x+2tfuRL8MdW6rv5vb2+CBEGTGwBdux0OIERcOS+RzxeK80k2DsQ==",
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/cache": "^11.13.5",
"@emotion/cache": "^11.14.0",
"@emotion/serialize": "^1.3.3",
"@emotion/use-insertion-effect-with-fallbacks": "^1.1.0",
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"hoist-non-react-statics": "^3.3.1"
@@ -429,16 +429,16 @@
"license": "MIT"
},
"node_modules/@emotion/styled": {
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.5.tgz",
"integrity": "sha512-gnOQ+nGLPvDXgIx119JqGalys64lhMdnNQA9TMxhDA4K0Hq5+++OE20Zs5GxiCV9r814xQ2K5WmtofSpHVW6BQ==",
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz",
"integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/is-prop-valid": "^1.3.0",
"@emotion/serialize": "^1.3.3",
"@emotion/use-insertion-effect-with-fallbacks": "^1.1.0",
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
"@emotion/utils": "^1.4.2"
},
"peerDependencies": {
@@ -458,9 +458,9 @@
"license": "MIT"
},
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz",
"integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
"integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.0"
@@ -1071,15 +1071,15 @@
}
},
"node_modules/@mui/base": {
"version": "5.0.0-beta.64",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.64.tgz",
"integrity": "sha512-nu663PoZs/Pee0fkPYkjUADfT+AAi2QWvvHghDhLeSx8sa3i+GGaOoUsFmB4CPlyYqWfq9hRGA7H1T3d6VrGgw==",
"version": "5.0.0-beta.67",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.67.tgz",
"integrity": "sha512-pwjibXqBrSw1Z1fZEIhWJnMJHSvh3BLz0pZcrX/x01B/BVf6QUbFlGVV/k8KCsZ9sPzln1l8bBJ+ynIIE9LtjA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@floating-ui/react-dom": "^2.1.1",
"@mui/types": "^7.2.19",
"@mui/utils": "^6.1.10",
"@mui/types": "^7.2.20",
"@mui/utils": "^6.2.1",
"@popperjs/core": "^2.11.8",
"clsx": "^2.1.1",
"prop-types": "^15.8.1"
@@ -1092,9 +1092,9 @@
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -1103,9 +1103,9 @@
}
},
"node_modules/@mui/core-downloads-tracker": {
"version": "6.1.10",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.10.tgz",
"integrity": "sha512-LY5wdiLCBDY7u+Od8UmFINZFGN/5ZU90fhAslf/ZtfP+5RhuY45f679pqYIxe0y54l6Gkv9PFOc8Cs10LDTBYg==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.2.1.tgz",
"integrity": "sha512-U/8vS1+1XiHBnnRRESSG1gvr6JDHdPjrpnW6KEebkAQWBn6wrpbSF/XSZ8/vJIRXH5NyDmMHi4Ro5Q70//JKhA==",
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -1113,9 +1113,9 @@
}
},
"node_modules/@mui/icons-material": {
"version": "6.1.10",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.10.tgz",
"integrity": "sha512-G6P1BCSt6EQDcKca47KwvKjlqgOXFbp2I3oWiOlFgKYTANBH89yk7ttMQ5ysqNxSYAB+4TdM37MlPYp4+FkVrQ==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.2.1.tgz",
"integrity": "sha512-bP0XtW+t5KFL+wjfQp2UctN/8CuWqF1qaxbYuCAsJhL+AzproM8gGOh2n8sNBcrjbVckzDNqaXqxdpn+OmoWug==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0"
@@ -1128,7 +1128,7 @@
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@mui/material": "^6.1.10",
"@mui/material": "^6.2.1",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
@@ -1139,16 +1139,16 @@
}
},
"node_modules/@mui/lab": {
"version": "6.0.0-beta.18",
"resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.18.tgz",
"integrity": "sha512-O7jNn36Jb0530NOZeFLj33RGB57x3kfyiYOaj5sL/j/Pmq9T0tonKMkoW/AUCucmBa7RuEzEYMyqBpfqminebA==",
"version": "6.0.0-beta.20",
"resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.20.tgz",
"integrity": "sha512-Sg3aTrSSsfXcSHwKhnZbXmz7kUZzvXww3vO4p8U/DbMVSGJxll1GRpKNQkFLpZy2y7ZBTgKaZTbPYw4Q6UKrsA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/base": "5.0.0-beta.64",
"@mui/system": "^6.1.10",
"@mui/types": "^7.2.19",
"@mui/utils": "^6.1.10",
"@mui/base": "5.0.0-beta.67",
"@mui/system": "^6.2.1",
"@mui/types": "^7.2.20",
"@mui/utils": "^6.2.1",
"clsx": "^2.1.1",
"prop-types": "^15.8.1"
},
@@ -1162,8 +1162,8 @@
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@mui/material": "^6.1.10",
"@mui/material-pigment-css": "^6.1.10",
"@mui/material": "^6.2.1",
"@mui/material-pigment-css": "^6.2.1",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -1184,22 +1184,22 @@
}
},
"node_modules/@mui/material": {
"version": "6.1.10",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.10.tgz",
"integrity": "sha512-txnwYObY4N9ugv5T2n5h1KcbISegZ6l65w1/7tpSU5OB6MQCU94YkP8n/3slDw2KcEfRk4+4D8EUGfhSPMODEQ==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.2.1.tgz",
"integrity": "sha512-7VlKGsRKsy1bOSOPaSNgpkzaL+0C7iWAVKd2KYyAvhR9fTLJtiAMpq+KuzgEh1so2mtvQERN0tZVIceWMiIesw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/core-downloads-tracker": "^6.1.10",
"@mui/system": "^6.1.10",
"@mui/types": "^7.2.19",
"@mui/utils": "^6.1.10",
"@mui/core-downloads-tracker": "^6.2.1",
"@mui/system": "^6.2.1",
"@mui/types": "^7.2.20",
"@mui/utils": "^6.2.1",
"@popperjs/core": "^2.11.8",
"@types/react-transition-group": "^4.4.11",
"@types/react-transition-group": "^4.4.12",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1",
"react-is": "^18.3.1",
"react-is": "^19.0.0",
"react-transition-group": "^4.4.5"
},
"engines": {
@@ -1212,7 +1212,7 @@
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@mui/material-pigment-css": "^6.1.10",
"@mui/material-pigment-css": "^6.2.1",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -1232,14 +1232,20 @@
}
}
},
"node_modules/@mui/material/node_modules/react-is": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
"integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==",
"license": "MIT"
},
"node_modules/@mui/private-theming": {
"version": "6.1.10",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.10.tgz",
"integrity": "sha512-DqgsH0XFEweeG3rQfVkqTkeXcj/E76PGYWag8flbPdV8IYdMo+DfVdFlZK8JEjsaIVD2Eu1kJg972XnH5pfnBQ==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.2.1.tgz",
"integrity": "sha512-u1y0gpcfrRRxCcIdVeU5eIvkinA82Q8ft178WUNYuoFQrsOrXdlBdZlRVi+eYuUFp1iXI55Cud7sMZZtETix5Q==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/utils": "^6.1.10",
"@mui/utils": "^6.2.1",
"prop-types": "^15.8.1"
},
"engines": {
@@ -1260,9 +1266,9 @@
}
},
"node_modules/@mui/styled-engine": {
"version": "6.1.10",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.10.tgz",
"integrity": "sha512-+NV9adKZYhslJ270iPjf2yzdVJwav7CIaXcMlPSi1Xy1S/zRe5xFgZ6BEoMdmGRpr34lIahE8H1acXP2myrvRw==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.2.1.tgz",
"integrity": "sha512-6R3OgYw6zgCZWFYYMfxDqpGfJA78mUTOIlUDmmJlr60ogVNCrM87X0pqx5TbZ2OwUyxlJxN9qFgRr+J9H6cOBg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
@@ -1294,16 +1300,16 @@
}
},
"node_modules/@mui/system": {
"version": "6.1.10",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.10.tgz",
"integrity": "sha512-5YNIqxETR23SIkyP7MY2fFnXmplX/M4wNi2R+10AVRd3Ub+NLctWY/Vs5vq1oAMF0eSDLhRTGUjaUe+IGSfWqg==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-6.2.1.tgz",
"integrity": "sha512-0lc8CbBP4WAAF+SmGMFJI9bpIyQvW3zvwIDzLsb26FIB/4Z0pO7qGe8mkAl0RM63Vb37899qxnThhHKgAAdy6w==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/private-theming": "^6.1.10",
"@mui/styled-engine": "^6.1.10",
"@mui/types": "^7.2.19",
"@mui/utils": "^6.1.10",
"@mui/private-theming": "^6.2.1",
"@mui/styled-engine": "^6.2.1",
"@mui/types": "^7.2.20",
"@mui/utils": "^6.2.1",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
@@ -1334,9 +1340,9 @@
}
},
"node_modules/@mui/types": {
"version": "7.2.19",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.19.tgz",
"integrity": "sha512-6XpZEM/Q3epK9RN8ENoXuygnqUQxE+siN/6rGRi2iwJPgBUR25mphYQ9ZI87plGh58YoZ5pp40bFvKYOCDJ3tA==",
"version": "7.2.20",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.20.tgz",
"integrity": "sha512-straFHD7L8v05l/N5vcWk+y7eL9JF0C2mtph/y4BPm3gn2Eh61dDwDB65pa8DLss3WJfDXYC7Kx5yjP0EmXpgw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -1348,17 +1354,17 @@
}
},
"node_modules/@mui/utils": {
"version": "6.1.10",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.10.tgz",
"integrity": "sha512-1ETuwswGjUiAf2dP9TkBy8p49qrw2wXa+RuAjNTRE5+91vtXJ1HKrs7H9s8CZd1zDlQVzUcUAPm9lpQwF5ogTw==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.2.1.tgz",
"integrity": "sha512-ubLqGIMhKUH2TF/Um+wRzYXgAooQw35th+DPemGrTpgrZHpOgcnUDIDbwsk1e8iQiuJ3mV/ErTtcQrecmlj5cg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/types": "^7.2.19",
"@types/prop-types": "^15.7.13",
"@mui/types": "^7.2.20",
"@types/prop-types": "^15.7.14",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"react-is": "^18.3.1"
"react-is": "^19.0.0"
},
"engines": {
"node": ">=14.0.0"
@@ -1377,10 +1383,16 @@
}
}
},
"node_modules/@mui/utils/node_modules/react-is": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
"integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==",
"license": "MIT"
},
"node_modules/@mui/x-charts": {
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.23.0.tgz",
"integrity": "sha512-lXG+vRMFvisFaj93LlglZij7e5NEHptXQG0x9EshSI8uIm8HN9Xp77qIdE95xLyltImakuFCeJaEk9oaRMBTpA==",
"version": "7.23.2",
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.23.2.tgz",
"integrity": "sha512-wLeogvQZZtyrAOdG06mDzIQSHBSAB09Uy16AYRUcMxVObi7Fs0i3TJUMpQHMYz1/1DvE1u8zstDgVpVfk8/iCA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
@@ -1436,9 +1448,9 @@
}
},
"node_modules/@mui/x-data-grid": {
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.23.0.tgz",
"integrity": "sha512-nypSz/7j0HPvW7tRPcZAlQADOiRAE4jTIcxwwJUPLtU17EPJOiw1iB29SRYtUThw4f3aXETPAeT4fzgagpuiKg==",
"version": "7.23.3",
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.23.3.tgz",
"integrity": "sha512-EiM5kut6N/0o0iEYx8A7M3fJqknAa1kcPvGhlX3hH50ERLDeuJaqoKzvRYLBbYKWydHIc+0hHIFcK5oQTXLenw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
@@ -1473,9 +1485,9 @@
}
},
"node_modules/@mui/x-date-pickers": {
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.23.0.tgz",
"integrity": "sha512-Db9ElibVYHluXLVsRLfFwlYkL6/3NNE5AosSZiTx+Gw7uix/Z3pdjyHeA3ab65fU1tCk08XHY0PU6LQFifYB2g==",
"version": "7.23.3",
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.23.3.tgz",
"integrity": "sha512-bjTYX/QzD5ZhVZNNnastMUS3j2Hy4p4IXmJgPJ0vKvQBvUdfEO+ZF42r3PJNNde0FVT1MmTzkmdTlz0JZ6ukdw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
@@ -1679,9 +1691,9 @@
}
},
"node_modules/@reduxjs/toolkit": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.4.0.tgz",
"integrity": "sha512-wJZEuSKj14tvNfxiIiJws0tQN77/rDqucBq528ApebMIRHyWpCanJVQRxQ8WWZC19iCDKxDsGlbAir3F1layxA==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.0.tgz",
"integrity": "sha512-awNe2oTodsZ6LmRqmkFhtb/KH03hUhxOamEQy411m3Njj3BbFvoBovxo4Q1cBWnV1ErprVj9MlF0UPXkng0eyg==",
"license": "MIT",
"dependencies": {
"immer": "^10.0.3",
@@ -1690,7 +1702,7 @@
"reselect": "^5.1.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18",
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
},
"peerDependenciesMeta": {
@@ -2354,15 +2366,15 @@
"license": "MIT"
},
"node_modules/@types/prop-types": {
"version": "15.7.13",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.13",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.13.tgz",
"integrity": "sha512-ii/gswMmOievxAJed4PAHT949bpYjPKXvXo1v6cRB/kqc2ZR4n+SgyCyvyc5Fec5ez8VnUumI1Vk7j6fRyRogg==",
"version": "18.3.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -2370,28 +2382,28 @@
}
},
"node_modules/@types/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==",
"version": "18.3.5",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz",
"integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/react": "*"
"peerDependencies": {
"@types/react": "^18.0.0"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.11",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz",
"integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==",
"version": "4.4.12",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
"integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
"license": "MIT",
"dependencies": {
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
"integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==",
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
"node_modules/@ungap/structured-clone": {
@@ -3551,9 +3563,9 @@
}
},
"node_modules/eslint-plugin-react-hooks": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0.tgz",
"integrity": "sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz",
"integrity": "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4887,16 +4899,15 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -5333,17 +5344,17 @@
"license": "MIT"
},
"node_modules/react-redux": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz",
"integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==",
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
"dependencies": {
"@types/use-sync-external-store": "^0.0.3",
"use-sync-external-store": "^1.0.0"
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"@types/react": "^18.2.25",
"react": "^18.0",
"@types/react": "^18.2.25 || ^19",
"react": "^18.0 || ^19",
"redux": "^5.0.0"
},
"peerDependenciesMeta": {
@@ -5366,9 +5377,9 @@
}
},
"node_modules/react-router": {
"version": "6.28.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.0.tgz",
"integrity": "sha512-HrYdIFqdrnhDw0PqG/AKjAqEqM7AvxCz0DQ4h2W8k6nqmc5uRBYDag0SBxx9iYz5G8gnuNVLzUe13wl9eAsXXg==",
"version": "6.28.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.1.tgz",
"integrity": "sha512-2omQTA3rkMljmrvvo6WtewGdVh45SpL9hGiCI9uUrwGGfNFDIvGK4gYJsKlJoNVi6AQZcopSCballL+QGOm7fA==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.21.0"
@@ -5381,13 +5392,13 @@
}
},
"node_modules/react-router-dom": {
"version": "6.28.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.28.0.tgz",
"integrity": "sha512-kQ7Unsl5YdyOltsPGl31zOjLrDv+m2VcIEcIHqYYD3Lp0UppLjrzcfJqDJwXxFw3TH/yvapbnUvPlAj7Kx5nbg==",
"version": "6.28.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.28.1.tgz",
"integrity": "sha512-YraE27C/RdjcZwl5UCqF/ffXnZDxpJdk9Q6jw38SZHjXs7NNdpViq2l2c7fO7+4uWaEfcwfGCv3RSg4e1By/fQ==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.21.0",
"react-router": "6.28.0"
"react-router": "6.28.1"
},
"engines": {
"node": ">=14.0.0"
@@ -5442,9 +5453,9 @@
}
},
"node_modules/recharts": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.14.1.tgz",
"integrity": "sha512-xtWulflkA+/xu4/QClBdtZYN30dbvTHjxjkh5XTMrH/CQ3WGDDPHHa/LLKCbgoqz0z3UaSH2/blV1i6VNMeh1g==",
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.0.tgz",
"integrity": "sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==",
"license": "MIT",
"dependencies": {
"clsx": "^2.0.0",
@@ -5460,8 +5471,8 @@
"node": ">=14"
},
"peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/recharts-scale": {
@@ -6163,12 +6174,12 @@
}
},
"node_modules/use-sync-external-store": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
"integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
"integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/victory-vendor": {
+8 -8
View File
@@ -14,24 +14,24 @@
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@fontsource/roboto": "^5.0.13",
"@mui/icons-material": "6.1.10",
"@mui/lab": "6.0.0-beta.18",
"@mui/material": "6.1.10",
"@mui/icons-material": "6.2.1",
"@mui/lab": "6.0.0-beta.20",
"@mui/material": "6.2.1",
"@mui/x-charts": "^7.5.1",
"@mui/x-data-grid": "7.23.0",
"@mui/x-date-pickers": "7.23.0",
"@reduxjs/toolkit": "2.4.0",
"@mui/x-data-grid": "7.23.3",
"@mui/x-date-pickers": "7.23.3",
"@reduxjs/toolkit": "2.5.0",
"axios": "^1.7.4",
"dayjs": "1.11.13",
"joi": "17.13.3",
"jwt-decode": "^4.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "9.1.2",
"react-redux": "9.2.0",
"react-router": "^6.23.0",
"react-router-dom": "^6.23.1",
"react-toastify": "^10.0.5",
"recharts": "2.14.1",
"recharts": "2.15.0",
"redux-persist": "6.0.0",
"vite-plugin-svgr": "^4.2.0"
},
+3 -195
View File
@@ -1,37 +1,8 @@
import { useEffect } from "react";
import { Routes, Route, Navigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import "react-toastify/dist/ReactToastify.css";
import { ToastContainer } from "react-toastify";
import NotFound from "./Pages/NotFound";
import Login from "./Pages/Auth/Login";
import Register from "./Pages/Auth/Register/Register";
import Account from "./Pages/Account";
import Monitors from "./Pages/Monitors/Home";
import CreateMonitor from "./Pages/Monitors/CreateMonitor";
import CreateInfrastructureMonitor from "./Pages/Infrastructure/CreateMonitor";
import Incidents from "./Pages/Incidents";
import Status from "./Pages/Status";
import Integrations from "./Pages/Integrations";
import Settings from "./Pages/Settings";
import ForgotPassword from "./Pages/Auth/ForgotPassword";
import CheckEmail from "./Pages/Auth/CheckEmail";
import SetNewPassword from "./Pages/Auth/SetNewPassword";
import NewPasswordConfirmed from "./Pages/Auth/NewPasswordConfirmed";
import ProtectedRoute from "./Components/ProtectedRoute";
import Details from "./Pages/Monitors/Details";
import AdvancedSettings from "./Pages/AdvancedSettings";
import Maintenance from "./Pages/Maintenance";
import Configure from "./Pages/Monitors/Configure";
import PageSpeed from "./Pages/PageSpeed";
import CreatePageSpeed from "./Pages/PageSpeed/CreatePageSpeed";
import CreateNewMaintenanceWindow from "./Pages/Maintenance/CreateMaintenance";
import PageSpeedDetails from "./Pages/PageSpeed/Details";
import PageSpeedConfigure from "./Pages/PageSpeed/Configure";
import HomeLayout from "./Components/Layouts/HomeLayout";
import withAdminCheck from "./Components/HOC/withAdminCheck";
import withAdminProp from "./Components/HOC/withAdminProp";
import { ThemeProvider } from "@emotion/react";
import lightTheme from "./Utils/Theme/lightTheme";
import darkTheme from "./Utils/Theme/darkTheme";
@@ -39,18 +10,9 @@ import { CssBaseline, GlobalStyles } from "@mui/material";
import { getAppSettings } from "./Features/Settings/settingsSlice";
import { logger } from "./Utils/Logger"; // Import the logger
import { networkService } from "./main";
import { Infrastructure } from "./Pages/Infrastructure";
import InfrastructureDetails from "./Pages/Infrastructure/Details";
import { Routes } from "./Routes";
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 InfrastructureDetailsWithAdminProp = withAdminProp(InfrastructureDetails);
const mode = useSelector((state) => state.ui.mode);
const { authToken } = useSelector((state) => state.auth);
const dispatch = useDispatch();
@@ -82,161 +44,7 @@ function App() {
};
}}
/>
{/* Extract Routes to Routes */}
<Routes>
<Route
exact
path="/"
element={<HomeLayout />}
>
<Route
exact
path="/"
element={<Navigate to="/monitors" />}
/>
<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="pagespeed"
element={<ProtectedRoute Component={PageSpeedWithAdminProp} />}
/>
<Route
path="pagespeed/create"
element={<ProtectedRoute Component={CreatePageSpeed} />}
/>
<Route
path="pagespeed/:monitorId"
element={<ProtectedRoute Component={PageSpeedDetailsWithAdminProp} />}
/>
<Route
path="pagespeed/configure/:monitorId"
element={<ProtectedRoute Component={PageSpeedConfigure} />}
/>
<Route
path="infrastructure"
element={<ProtectedRoute Component={Infrastructure} />}
/>
<Route
path="infrastructure/create"
element={<ProtectedRoute Component={CreateInfrastructureMonitor} />}
/>
<Route
path="infrastructure/:monitorId"
element={<ProtectedRoute Component={InfrastructureDetailsWithAdminProp} />}
/>
<Route
path="incidents/:monitorId?"
element={<ProtectedRoute Component={Incidents} />}
/>
<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>
<Route
exact
path="/login"
element={<Login />}
/>
<Route
exact
path="/register"
element={<AdminCheckedRegister />}
/>
<Route
exact
path="/register/:token"
element={<Register />}
/>
<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>
<Routes />
<ToastContainer />
</ThemeProvider>
);
+4 -24
View File
@@ -2,28 +2,7 @@ import { Avatar as MuiAvatar } from "@mui/material";
import PropTypes from "prop-types";
import { useSelector } from "react-redux";
import { useEffect, useState } from "react";
/**
* Generates a color based on the input string.
* @param {string} string - The input string to generate the color from.
* @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 color = "#";
for (i = 0; i < 3; i += 1) {
const value = (hash >> (i * 8)) & 0xff;
color += `00${value.toString(16)}`.slice(-2);
}
return color;
};
import { useTheme } from "@emotion/react";
/**
* @component
* @param {Object} props
@@ -38,6 +17,7 @@ const stringToColor = (string) => {
const Avatar = ({ src, small, sx }) => {
const { user } = useSelector((state) => state.auth);
const theme = useTheme();
const style = small ? { width: 32, height: 32 } : { width: 64, height: 64 };
const border = small ? 1 : 3;
@@ -57,7 +37,7 @@ const Avatar = ({ src, small, sx }) => {
fontSize: small ? "16px" : "22px",
color: "white",
fontWeight: 400,
backgroundColor: stringToColor(`${user?.firstName} ${user?.lastName}`),
backgroundColor: theme.palette.primary.main, // Same BG color as checkmate BG in sidebar
display: "inline-flex",
"&::before": {
content: `""`,
@@ -74,7 +54,7 @@ const Avatar = ({ src, small, sx }) => {
}}
>
{user.firstName?.charAt(0)}
{user.lastName?.charAt(0)}
{user.lastName?.charAt(0) || ''}
</MuiAvatar>
);
};
+16 -3
View File
@@ -115,7 +115,7 @@ TablePaginationActions.propTypes = {
* @param {boolean} [props.paginated=false] - Flag to enable pagination.
* @param {boolean} [props.reversed=false] - Flag to enable reverse order.
* @param {number} props.rowsPerPage- Number of rows per page (table).
*
* @param {string} props.emptyMessage - Message to display when there is no data.
* @example
* const data = {
* cols: [
@@ -149,7 +149,7 @@ TablePaginationActions.propTypes = {
* <BasicTable data={data} rows={rows} paginated={true} />
*/
const BasicTable = ({ data, paginated, reversed, table }) => {
const BasicTable = ({ data, paginated, reversed, table, emptyMessage = "No data" }) => {
const DEFAULT_ROWS_PER_PAGE = 5;
const theme = useTheme();
const dispatch = useDispatch();
@@ -216,7 +216,9 @@ const BasicTable = ({ data, paginated, reversed, table }) => {
sx={{
cursor: row.handleClick ? "pointer" : "default",
"&:hover": {
backgroundColor: theme.palette.background.accent,
filter: "brightness(.75)",
opacity: 0.75,
transition: "filter 0.3s ease, opacity 0.3s ease",
},
}}
key={row.id}
@@ -228,6 +230,16 @@ const BasicTable = ({ data, paginated, reversed, table }) => {
</TableRow>
);
})}
{displayData.length === 0 && (
<TableRow>
<TableCell
sx={{ textAlign: "center" }}
colSpan={data.cols.length}
>
{emptyMessage}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
@@ -317,6 +329,7 @@ BasicTable.propTypes = {
reversed: PropTypes.bool,
rowPerPage: PropTypes.number,
table: PropTypes.string,
emptyMessage: PropTypes.string,
};
export default BasicTable;
@@ -1,12 +1,15 @@
import { Stack, styled } from "@mui/material";
export const ConfigBox = styled(Stack)(({ theme }) => ({
const ConfigBox = styled(Stack)(({ theme }) => ({
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
gap: theme.spacing(20),
backgroundColor: theme.palette.background.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.light,
borderRadius: theme.spacing(2),
backgroundColor: theme.palette.background.main,
"& > *": {
paddingTop: theme.spacing(12),
paddingBottom: theme.spacing(18),
@@ -24,15 +27,12 @@ export const ConfigBox = styled(Stack)(({ theme }) => ({
paddingRight: theme.spacing(20),
paddingLeft: theme.spacing(18),
},
"& h2": {
"& h1, & h2": {
color: theme.palette.text.secondary,
fontSize: 15,
fontWeight: 600,
},
"& h3, & p": {
color: theme.palette.text.tertiary,
},
"& p": {
fontSize: 13,
color: theme.palette.text.tertiary,
},
}));
export default ConfigBox;
@@ -1,25 +0,0 @@
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);
return (
<WrappedComponent
{...props}
isAdmin={isAdmin}
/>
);
};
const wrappedComponentName =
WrappedComponent.displayName || WrappedComponent.name || "Component";
WithAdminProp.displayName = `WithAdminProp(${wrappedComponentName})`;
return WithAdminProp;
};
export default withAdminProp;
+77
View File
@@ -0,0 +1,77 @@
import { Box, styled } from "@mui/material";
import PropTypes from "prop-types";
/**
* IconBox - A styled box component for rendering icons with consistent sizing and styling
*
* @component
* @param {Object} [props] - Configuration options for the IconBox
* @param {number} [props.height=34] - Height of the icon box
* @param {number} [props.width=34] - Width of the icon box
* @param {number} [props.minWidth=34] - Minimum width of the icon box
* @param {number} [props.borderRadius=4] - Border radius of the icon box
* @param {number} [props.svgWidth=20] - Width of the SVG icon
* @param {number} [props.svgHeight=20] - Height of the SVG icon
*
* @example
* // Basic usage
* <IconBox>
* <SomeIcon />
* </IconBox>
*
* @example
* // Customized usage
* <IconBox
* height={40}
* width={40}
* svgWidth={24}
* svgHeight={24}
* >
* <CustomIcon />
* </IconBox>
*
* @returns {React.ReactElement} A styled box containing an icon
*/
const IconBox = styled(Box)(
({
theme,
height = 34,
width = 34,
minWidth = 34,
borderRadius = 4,
svgWidth = 20,
svgHeight = 20,
}) => ({
height: height,
minWidth: minWidth,
width: width,
position: "relative",
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.dark,
borderRadius: borderRadius,
backgroundColor: theme.palette.background.accent,
"& svg": {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: svgWidth,
height: svgHeight,
"& path": {
stroke: theme.palette.text.tertiary,
},
},
})
);
IconBox.propTypes = {
height: PropTypes.number,
width: PropTypes.number,
minWidth: PropTypes.number,
borderRadius: PropTypes.number,
svgWidth: PropTypes.number,
svgHeight: PropTypes.number,
};
export default IconBox;
+36 -17
View File
@@ -3,33 +3,50 @@ import { FormControlLabel, Checkbox as MuiCheckbox } from "@mui/material";
import { useTheme } from "@emotion/react";
import CheckboxOutline from "../../../assets/icons/checkbox-outline.svg?react";
import CheckboxFilled from "../../../assets/icons/checkbox-filled.svg?react";
import "./index.css";
/**
* @param {Object} props
* @param {string} props.id - The id attribute for the checkbox input.
* @param {string} props.label - The label to display next to the checkbox.
* @param {('small' | 'medium' | 'large')} - The size of the checkbox.
* @param {boolean} props.isChecked - Whether the checkbox is checked or not.
* @param {string} [props.value] - The value of the checkbox input.
* @param {function} [props.onChange] - The function to call when the checkbox value changes.
* @param {boolean} [props.isDisabled] - Whether the checkbox is disabled or not.
* Checkbox Component
*
* @returns {JSX.Element}
* A customized checkbox component using Material-UI that supports custom sizing,
* disabled states, and custom icons.
*
* @component
* @param {Object} props - Component properties
* @param {string} props.id - Unique identifier for the checkbox input
* @param {string} [props.name] - Optional name attribute for the checkbox
* @param {(string|React.ReactNode)} props.label - Label text or node for the checkbox
* @param {('small'|'medium'|'large')} [props.size='medium'] - Size of the checkbox icon
* @param {boolean} props.isChecked - Current checked state of the checkbox
* @param {string} [props.value] - Optional value associated with the checkbox
* @param {Function} [props.onChange] - Callback function triggered when checkbox state changes
* @param {boolean} [props.isDisabled] - Determines if the checkbox is disabled
*
* @returns {React.ReactElement} Rendered Checkbox component
*
* @example
* // Basic usage
* <Checkbox
* id="checkbox-id"
* label="Ping monitoring"
* isChecked={checks.type === "ping"}
* value="ping"
* onChange={handleChange}
* id="terms-checkbox"
* label="I agree to terms"
* isChecked={agreed}
* onChange={handleAgree}
* />
*
* @example
* // With custom size and disabled state
* <Checkbox
* id="advanced-checkbox"
* label="Advanced Option"
* size="large"
* isChecked={isAdvanced}
* isDisabled={!canModify}
* onChange={handleAdvancedToggle}
* />
*/
const Checkbox = ({
id,
name,
label,
size = "medium",
isChecked,
@@ -46,6 +63,7 @@ const Checkbox = ({
control={
<MuiCheckbox
checked={isDisabled ? false : isChecked}
name={name}
value={value}
onChange={onChange}
icon={<CheckboxOutline />}
@@ -54,7 +72,7 @@ const Checkbox = ({
"aria-label": "controlled checkbox",
id: id,
}}
sx={{
sx={{
"&:hover": { backgroundColor: "transparent" },
"& svg": { width: sizes[size], height: sizes[size] },
alignSelf: "flex-start",
@@ -89,6 +107,7 @@ const Checkbox = ({
Checkbox.propTypes = {
id: PropTypes.string.isRequired,
name: PropTypes.string,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
size: PropTypes.oneOf(["small", "medium", "large"]),
isChecked: PropTypes.bool.isRequired,
@@ -42,6 +42,9 @@ const Radio = (props) => {
width: 16,
height: 16,
boxShadow: `inset 0 0 0 1px ${theme.palette.secondary.main}`,
"&:not(.Mui-checked)": {
boxShadow: `inset 0 0 0 1px ${theme.palette.text.primary}70`, // Use theme text color for the outline
},
mt: theme.spacing(0.5),
}}
/>
@@ -40,6 +40,7 @@ const SearchAdornment = () => {
//TODO keep search state inside of component
const Search = ({
label,
id,
options,
filteredBy,
@@ -76,6 +77,14 @@ const Search = ({
getOptionLabel={(option) => option[filteredBy]}
renderInput={(params) => (
<Stack>
<Typography
component="h3"
fontSize={"var(--env-var-font-size-medium)"}
color={theme.palette.text.secondary}
fontWeight={500}
>
{label}
</Typography>
<TextField
{...params}
error={Boolean(error)}
@@ -93,6 +102,9 @@ const Search = ({
{
borderColor: theme.palette.border.light,
},
"& .MuiOutlinedInput-root": {
paddingY: 0,
},
}}
/>
{error && (
@@ -49,6 +49,7 @@ const Select = ({
onBlur,
sx,
name = "",
labelControlSpacing = 2,
}) => {
const theme = useTheme();
const itemStyles = {
@@ -57,10 +58,10 @@ const Select = ({
borderRadius: theme.shape.borderRadius,
margin: theme.spacing(2),
};
return (
<Stack
gap={theme.spacing(2)}
gap={theme.spacing(labelControlSpacing)}
className="select-wrapper"
>
{label && (
@@ -144,6 +145,7 @@ Select.propTypes = {
onChange: PropTypes.func.isRequired,
onBlur: PropTypes.func,
sx: PropTypes.object,
labelControlSpacing: PropTypes.number,
};
export default Select;
+3 -3
View File
@@ -141,9 +141,9 @@ const StatusLabel = ({ status, text, customStyles }) => {
borderColor: theme.palette.warning.light,
},
pending: {
dotColor: theme.palette.warning.main,
bgColor: theme.palette.warning.dark,
borderColor: theme.palette.warning.light,
dotColor: theme.palette.text.secondary,
bgColor: theme.palette.background.main,
borderColor: theme.palette.border.dark,
},
"cannot resolve": {
dotColor: theme.palette.unresolved.main,
@@ -3,21 +3,20 @@ import { useSelector } from "react-redux";
import PropTypes from "prop-types";
/**
* ProtectedRoute is a higher-order component that wraps the `Route` component from `react-router-dom`
* to create a protected route. It uses Redux to check if the user is authenticated. If the user is
* authenticated, it renders the component passed to it; otherwise, it redirects the user to the login page.
* ProtectedRoute is a wrapper component that ensures only authenticated users
* can access the wrapped content. It checks authentication status (e.g., from Redux or Context).
* If the user is authenticated, it renders the children; otherwise, it redirects to the login page.
*
* @param {Object} props - The props passed to the ProtectedRoute component.
* @param {React.ComponentType} props.Component - The component to render if the user is authenticated.
* @param {Object} rest - The remaining props passed to the Route component.
* @returns {React.ReactElement} A Route component that conditionally renders the passed Component or redirects to the login page.
* @param {React.ReactNode} props.children - The children to render if the user is authenticated.
* @returns {React.ReactElement} The children wrapped in a protected route or a redirect to the login page.
*/
const ProtectedRoute = ({ Component, ...rest }) => {
const ProtectedRoute = ({ children }) => {
const authState = useSelector((state) => state.auth);
return authState.authToken ? (
<Component {...rest} />
children
) : (
<Navigate
to="/login"
@@ -27,7 +26,7 @@ const ProtectedRoute = ({ Component, ...rest }) => {
};
ProtectedRoute.propTypes = {
Component: PropTypes.elementType.isRequired,
children: PropTypes.element.isRequired,
};
export default ProtectedRoute;
+49 -32
View File
@@ -21,13 +21,13 @@ import { useDispatch, useSelector } from "react-redux";
import { clearAuthState } from "../../Features/Auth/authSlice";
import { toggleSidebar } from "../../Features/UI/uiSlice";
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
import ThemeSwitch from "../ThemeSwitch";
import Avatar from "../Avatar";
import LockSvg from "../../assets/icons/lock.svg?react";
import UserSvg from "../../assets/icons/user.svg?react";
import TeamSvg from "../../assets/icons/user-two.svg?react";
import LogoutSvg from "../../assets/icons/logout.svg?react";
import Support from "../../assets/icons/support.svg?react";
import Dashboard from "../../assets/icons/dashboard.svg?react";
import Account from "../../assets/icons/user-edit.svg?react";
import Maintenance from "../../assets/icons/maintenance.svg?react";
import Monitors from "../../assets/icons/monitors.svg?react";
@@ -43,11 +43,12 @@ import DotsVertical from "../../assets/icons/dots-vertical.svg?react";
import ChangeLog from "../../assets/icons/changeLog.svg?react";
import Docs from "../../assets/icons/docs.svg?react";
import Folder from "../../assets/icons/folder.svg?react";
import ChatBubbleOutlineRoundedIcon from "@mui/icons-material/ChatBubbleOutlineRounded";
import "./index.css";
const menu = [
{ name: "Monitors", path: "monitors", icon: <Monitors /> },
{ name: "Uptime", path: "uptime", icon: <Monitors /> },
{ name: "Pagespeed", path: "pagespeed", icon: <PageSpeed /> },
{ name: "Infrastructure", path: "infrastructure", icon: <Integrations /> },
{ name: "Incidents", path: "incidents", icon: <Incidents /> },
@@ -63,20 +64,31 @@ const menu = [
{ name: "Team", path: "account/team", icon: <TeamSvg /> },
],
},
{
name: "Settings",
icon: <Settings />,
path: "settings",
},
{
name: "Other",
icon: <Folder />,
nested: [
{ name: "Settings", path: "settings", icon: <Settings /> },
{ name: "Support", path: "support", icon: <Support /> },
{
name: "Discussions",
path: "discussions",
icon: <ChatBubbleOutlineRoundedIcon />,
},
{ name: "Docs", path: "docs", icon: <Docs /> },
{ name: "Changelog", path: "changelog", icon: <ChangeLog /> },
],
},
];
/* TODO this could be a key in nested Path would be the link */
const URL_MAP = {
support: "https://github.com/bluewave-labs/bluewave-uptime/issues",
support: "https://discord.com/invite/NAb6H3UTjK",
discussions: "https://github.com/bluewave-labs/checkmate/discussions",
docs: "https://bluewavelabs.gitbook.io/checkmate",
changelog: "https://github.com/bluewave-labs/bluewave-uptime/releases",
};
@@ -177,13 +189,7 @@ function Sidebar() {
direction="row"
alignItems="center"
gap={theme.spacing(4)}
onClick={() =>
window.open(
"https://github.com/bluewave-labs/bluewave-uptime",
"_blank",
"noreferrer"
)
}
onClick={() => navigate("/")}
sx={{ cursor: "pointer" }}
>
<Stack
@@ -262,7 +268,11 @@ function Sidebar() {
Menu
</ListSubheader>
}
sx={{ px: theme.spacing(6) }}
sx={{
px: theme.spacing(6),
height: "100%",
overflow: "hidden",
}}
>
{menu.map((item) =>
item.path ? (
@@ -547,28 +557,35 @@ function Sidebar() {
{authState.user?.role}
</Typography>
</Box>
<Tooltip
title="Controls"
disableInteractive
<Stack
flexDirection={"row"}
marginLeft={"auto"}
columnGap={theme.spacing(2)}
>
<IconButton
sx={{
ml: "auto",
mr: "-8px",
"&:focus": { outline: "none" },
"& svg": {
width: "20px",
height: "20px",
},
"& svg path": {
stroke: theme.palette.other.icon,
},
}}
onClick={(event) => openPopup(event, "logout")}
<ThemeSwitch />
<Tooltip
title="Controls"
disableInteractive
>
<DotsVertical />
</IconButton>
</Tooltip>
<IconButton
sx={{
ml: "auto",
mr: "-8px",
"&:focus": { outline: "none" },
"& svg": {
width: "20px",
height: "20px",
},
"& svg path": {
stroke: theme.palette.other.icon,
},
}}
onClick={(event) => openPopup(event, "logout")}
>
<DotsVertical />
</IconButton>
</Tooltip>
</Stack>
</>
)}
<Menu
+74
View File
@@ -0,0 +1,74 @@
import { Box, Typography } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
/**
* StatBox Component
*
* A reusable component that displays a statistic with a heading and subheading
* in a styled box with a gradient background.
*
* @component
* @param {Object} props - The component props
* @param {string} props.heading - The primary heading/title of the statistic
* @param {string|React.ReactNode} props.subHeading - The value or description of the statistic
* @param {Object} [props.sx] - Additional custom styling to be applied to the box
*
* @example
* return (
* <StatBox
* heading="Total Users"
* subHeading="1,234"
* sx={{ width: 300 }}
* />
* )
*
* @returns {React.ReactElement} A styled box containing the statistic
*/
const StatBox = ({ heading, subHeading, sx }) => {
const theme = useTheme();
return (
<Box
sx={{
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
minWidth: 200,
width: 225,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.light,
borderRadius: 4,
backgroundColor: theme.palette.background.main,
background: `linear-gradient(340deg, ${theme.palette.background.accent} 20%, ${theme.palette.background.main} 45%)`,
"& h2": {
fontSize: 13,
fontWeight: 500,
color: theme.palette.text.secondary,
textTransform: "uppercase",
},
"& p": {
fontSize: 18,
color: theme.palette.text.primary,
marginTop: theme.spacing(2),
"& span": {
color: theme.palette.text.tertiary,
marginLeft: theme.spacing(2),
fontSize: 15,
},
},
...sx,
}}
>
<Typography component="h2">{heading}</Typography>
<Typography>{subHeading}</Typography>
</Box>
);
};
StatBox.propTypes = {
heading: PropTypes.string.isRequired,
subHeading: PropTypes.node.isRequired,
sx: PropTypes.object,
};
export default StatBox;
@@ -28,6 +28,9 @@ const PasswordPanel = () => {
const theme = useTheme();
const dispatch = useDispatch();
const SPACING_GAP = theme.spacing(12);
//redux state
const { authToken, isLoading } = useSelector((state) => state.auth);
@@ -139,7 +142,7 @@ const PasswordPanel = () => {
direction="row"
justifyContent={"flex-start"}
alignItems={"center"}
gap={theme.spacing(8)}
gap={SPACING_GAP}
flexWrap={"wrap"}
>
<Typography
@@ -164,7 +167,7 @@ const PasswordPanel = () => {
<Stack
direction="row"
alignItems={"center"}
gap={theme.spacing(8)}
gap={SPACING_GAP}
flexWrap={"wrap"}
>
<Typography
@@ -190,7 +193,7 @@ const PasswordPanel = () => {
<Stack
direction="row"
alignItems={"center"}
gap={theme.spacing(8)}
gap={SPACING_GAP}
flexWrap={"wrap"}
>
<Typography
@@ -29,6 +29,8 @@ import Dialog from "../../Dialog";
const ProfilePanel = () => {
const theme = useTheme();
const dispatch = useDispatch();
const SPACING_GAP = theme.spacing(12);
//redux state
const { user, authToken, isLoading } = useSelector((state) => state.auth);
@@ -223,9 +225,9 @@ const ProfilePanel = () => {
className="edit-profile-form"
noValidate
spellCheck="false"
gap={theme.spacing(20)}
gap={SPACING_GAP}
>
<Stack direction="row">
<Stack direction="row" gap={SPACING_GAP}>
<Box flex={0.9}>
<Typography component="h1">First name</Typography>
</Box>
@@ -240,7 +242,7 @@ const ProfilePanel = () => {
flex={1}
/>
</Stack>
<Stack direction="row">
<Stack direction="row" gap={SPACING_GAP}>
<Box flex={0.9}>
<Typography component="h1">Last name</Typography>
</Box>
@@ -255,7 +257,7 @@ const ProfilePanel = () => {
flex={1}
/>
</Stack>
<Stack direction="row">
<Stack direction="row" gap={SPACING_GAP}>
<Stack flex={0.9}>
<Typography component="h1">Email</Typography>
<Typography
@@ -275,7 +277,7 @@ const ProfilePanel = () => {
flex={1}
/>
</Stack>
<Stack direction="row">
<Stack direction="row" gap={SPACING_GAP}>
<Stack flex={0.9}>
<Typography component="h1">Your photo</Typography>
<Typography
@@ -361,7 +363,7 @@ const ProfilePanel = () => {
component="p"
sx={{ opacity: 0.6 }}
>
Note that deleting your account will remove all data from our system. This
Note that deleting your account will remove all data from the server. This
is permanent and non-recoverable.
</Typography>
</Box>
@@ -30,6 +30,8 @@ const TeamPanel = () => {
const theme = useTheme();
const SPACING_GAP = theme.spacing(12);
const { authToken, user } = useSelector((state) => state.auth);
//TODO
// const [orgStates, setOrgStates] = useState({
@@ -62,7 +64,7 @@ const TeamPanel = () => {
};
fetchTeam();
}, [user]);
}, [authToken]);
useEffect(() => {
let team = members;
@@ -79,8 +81,6 @@ const TeamPanel = () => {
{ id: 1, name: "NAME" },
{ id: 2, name: "EMAIL" },
{ id: 3, name: "ROLE" },
// FEATURE STILL TO BE IMPLEMENTED
// { id: 4, name: "ACTION" },
],
rows: team?.map((member, idx) => {
const roles = member.role.map((role) => roleMap[role]).join(",");
@@ -102,45 +102,19 @@ const TeamPanel = () => {
},
{ id: idx + 1, data: member.email },
{
// TODO - Add select dropdown
id: idx + 2,
data: roles,
},
// FEATURE STILL TO BE IMPLEMENTED
// {
// // TODO - Add delete onClick
// id: idx + 3,
// data: (
// <IconButton
// aria-label="remove member"
// sx={{
// "&:focus": {
// outline: "none",
// },
// }}
// >
// <Remove />
// </IconButton>
// ),
// },
],
};
}),
};
setTableData(data);
}, [members, filter]);
}, [filter, members, roleMap, theme]);
useEffect(() => {
setIsDisabled(Object.keys(errors).length !== 0 || toInvite.email === "");
}, [errors, toInvite.email]);
// RENAME ORGANIZATION
// const toggleEdit = () => {
// setOrgStates((prev) => ({ ...prev, isEdit: !prev.isEdit }));
// };
// const handleRename = () => {};
// INVITE MEMBER
const [isOpen, setIsOpen] = useState(false);
const handleChange = (event) => {
@@ -224,58 +198,11 @@ const TeamPanel = () => {
},
}}
>
{/* FEATURE STILL TO BE IMPLEMENTED */}
{/* <Stack component="form">
<Box sx={{ alignSelf: "flex-start" }}>
<Typography component="h1">Organization name</Typography>
</Box>
<Stack
direction="row"
justifyContent="flex-end"
alignItems="center"
sx={{ height: "34px" }}
>
<TextField
value={orgStates.name}
onChange={(event) =>
setOrgStates((prev) => ({
...prev,
name: event.target.value,
}))
}
disabled={!orgStates.isEdit}
sx={{
color: theme.palette.otherColors.bluishGray,
"& .Mui-disabled": {
WebkitTextFillColor: "initial !important",
},
"& .Mui-disabled fieldset": {
borderColor: "transparent !important",
},
}}
inputProps={{
sx: { textAlign: "end", padding: theme.spacing(4) },
}}
/>
<Button
level={orgStates.isEdit ? "secondary" : "tertiary"}
label={orgStates.isEdit ? "Save" : ""}
img={!orgStates.isEdit ? <EditSvg /> : ""}
onClick={() => toggleEdit()}
sx={{
minWidth: 0,
paddingX: theme.spacing(4),
ml: orgStates.isEdit ? theme.spacing(4) : 0,
}}
/>
</Stack>
</Stack>
<Divider aria-hidden="true" sx={{ marginY: theme.spacing(4) }} /> */}
<Stack
component="form"
noValidate
spellCheck="false"
gap={theme.spacing(12)}
gap={SPACING_GAP}
>
<Typography component="h1">Team members</Typography>
<Stack
@@ -326,6 +253,7 @@ const TeamPanel = () => {
paginated={false}
reversed={true}
table={"team"}
emptyMessage={"There are no team members with this role"}
/>
</Stack>
@@ -339,7 +267,7 @@ const TeamPanel = () => {
theme={theme}
>
<TextInput
marginBottom={theme.spacing(12)}
marginBottom={SPACING_GAP}
type="email"
id="input-team-member"
placeholder="Email"
@@ -0,0 +1,98 @@
import { useTheme } from "@mui/material";
import "./index.css";
const SunAndMoonIcon = () => {
const theme = useTheme();
return (
<svg
className="sun-and-moon"
aria-hidden="true"
width="24"
height="24"
viewBox="0 0 24 24"
>
<mask
className="moon"
id="moon-mask"
>
<rect
x="0"
y="0"
width="100%"
height="100%"
fill="#fff"
/>
<circle
cx="24"
cy="10"
r="6"
fill="#000"
/>
</mask>
<circle
className="sun"
cx="12"
cy="12"
r="6"
fill={theme.palette.text.secondary}
mask="url(#moon-mask)"
/>
<g
className="sun-beams"
stroke={theme.palette.text.secondary}
>
<line
x1="12"
y1="1"
x2="12"
y2="3"
/>
<line
x1="12"
y1="21"
x2="12"
y2="23"
/>
<line
x1="4.22"
y1="4.22"
x2="5.64"
y2="5.64"
/>
<line
x1="18.36"
y1="18.36"
x2="19.78"
y2="19.78"
/>
<line
x1="1"
y1="12"
x2="3"
y2="12"
/>
<line
x1="21"
y1="12"
x2="23"
y2="12"
/>
<line
x1="4.22"
y1="19.78"
x2="5.64"
y2="18.36"
/>
<line
x1="18.36"
y1="5.64"
x2="19.78"
y2="4.22"
/>
</g>
</svg>
);
};
export default SunAndMoonIcon;
@@ -0,0 +1,64 @@
.sun-and-moon > :is(.moon, .sun, .sun-beams) {
transform-origin: center;
}
.theme-toggle .sun-and-moon > .sun-beams {
stroke-width: 2px;
}
.theme-dark .sun-and-moon > .sun {
transform: scale(1.75);
}
.theme-dark .sun-and-moon > .sun-beams {
opacity: 0;
}
.theme-dark .sun-and-moon > .moon > circle {
transform: translateX(-7px);
}
@supports (cx: 1) {
.theme-dark .sun-and-moon > .moon > circle {
cx: 17;
transform: translateX(0);
}
}
@media (prefers-reduced-motion: no-preference) {
.sun-and-moon > .sun {
transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
.sun-and-moon > .sun-beams {
transition:
transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55),
opacity 0.5s cubic-bezier(0.25, 0.1, 0.25, 1);
}
.sun-and-moon .moon > circle {
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
@supports (cx: 1) {
.sun-and-moon .moon > circle {
transition: cx 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
}
.theme-dark .sun-and-moon > .sun {
transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
transition-duration: 0.25s;
transform: scale(1.75);
}
.theme-dark .sun-and-moon > .sun-beams {
transition-duration: 0.15s;
transform: rotateZ(-25deg);
}
.theme-dark .sun-and-moon > .moon > circle {
transition-duration: 0.5s;
transition-delay: 0.25s;
}
}
@@ -0,0 +1,47 @@
/**
* ThemeSwitch Component
* Dark and Light Theme Switch
* Original Code: https://web.dev/patterns/theming/theme-switch
* License: Apache License 2.0
* Copyright © Google LLC
*
* This code has been adapted for use in this project.
* Apache License: https://www.apache.org/licenses/LICENSE-2.0
*/
import { IconButton } from "@mui/material";
import SunAndMoonIcon from "./SunAndMoonIcon";
import { useDispatch, useSelector } from "react-redux";
import { setMode } from "../../Features/UI/uiSlice";
import "./index.css";
const ThemeSwitch = ({ width = 48, height = 48 }) => {
const mode = useSelector((state) => state.ui.mode);
const dispatch = useDispatch();
const toggleTheme = () => {
dispatch(setMode(mode === "light" ? "dark" : "light"));
};
return (
<IconButton
id="theme-toggle"
title="Toggles light & dark"
className={`theme-${mode}`}
aria-label="auto"
aria-live="polite"
onClick={toggleTheme}
sx={{
width,
height,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<SunAndMoonIcon />
</IconButton>
);
};
export default ThemeSwitch;
+1
View File
@@ -92,6 +92,7 @@ const Account = ({ open = "profile" }) => {
paddingY: theme.spacing(4),
fontWeight: 400,
marginRight: theme.spacing(8),
borderBottom: "2px solid transparent",
"&:focus": {
borderBottom: `2px solid ${theme.palette.border.light}`,
},
-359
View File
@@ -1,359 +0,0 @@
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography } from "@mui/material";
import TextInput from "../../Components/Inputs/TextInput";
import Link from "../../Components/Link";
import "./index.css";
import { useDispatch, useSelector } from "react-redux";
import { createToast } from "../../Utils/toastUtils";
import PropTypes from "prop-types";
import LoadingButton from "@mui/lab/LoadingButton";
import { ConfigBox } from "../Settings/styled";
import { useNavigate } from "react-router";
import { getAppSettings, updateAppSettings } from "../../Features/Settings/settingsSlice";
import { useState, useEffect } from "react";
import Select from "../../Components/Inputs/Select";
import { advancedSettingsValidation } from "../../Validation/validation";
import { buildErrors, hasValidationErrors } from "../../Validation/error";
const AdvancedSettings = ({ isAdmin }) => {
const navigate = useNavigate();
useEffect(() => {
if (!isAdmin) {
navigate("/");
}
}, [navigate, isAdmin]);
const [errors, setErrors] = useState({});
const theme = useTheme();
const { authToken } = useSelector((state) => state.auth);
const dispatch = useDispatch();
const settings = useSelector((state) => state.settings);
const [localSettings, setLocalSettings] = useState({
apiBaseUrl: "",
logLevel: "debug",
systemEmailHost: "",
systemEmailPort: "",
systemEmailAddress: "",
systemEmailPassword: "",
jwtTTLNum: 99,
jwtTTLUnits: "days",
jwtTTL: "99d",
dbType: "",
redisHost: "",
redisPort: "",
pagespeedApiKey: "",
});
const parseJWTTTL = (data) => {
if (data.jwtTTL) {
const len = data.jwtTTL.length;
data.jwtTTLNum = data.jwtTTL.substring(0, len - 1);
data.jwtTTLUnits = unitItems.filter(
(itm) => itm._id == data.jwtTTL.substring(len - 1)
)[0].name;
}
};
useEffect(() => {
const getSettings = async () => {
const action = await dispatch(getAppSettings({ authToken }));
if (action.payload.success) {
parseJWTTTL(action.payload.data);
setLocalSettings(action.payload.data);
} else {
createToast({ body: "Failed to get settings" });
}
};
getSettings();
}, [authToken, dispatch]);
const logItems = [
{ _id: 1, name: "none" },
{ _id: 2, name: "debug" },
{ _id: 3, name: "error" },
{ _id: 4, name: "warn" },
];
const logItemLookup = {
none: 1,
debug: 2,
error: 3,
warn: 4,
};
const unitItemLookup = {
days: "d",
hours: "h",
};
const unitItems = Object.keys(unitItemLookup).map((key) => ({
_id: unitItemLookup[key],
name: key,
}));
const handleLogLevel = (e) => {
const id = e.target.value;
const newLogLevel = logItems.find((item) => item._id === id).name;
setLocalSettings({ ...localSettings, logLevel: newLogLevel });
};
const handleJWTTTLUnits = (e) => {
const id = e.target.value;
const newUnits = unitItems.find((item) => item._id === id).name;
setLocalSettings({ ...localSettings, jwtTTLUnits: newUnits });
};
const handleBlur = (event) => {
const { value, id } = event.target;
const { error } = advancedSettingsValidation.validate(
{ [id]: value },
{
abortEarly: false,
}
);
setErrors((prev) => {
return buildErrors(prev, id, error);
});
};
const handleChange = (event) => {
const { value, id } = event.target;
setLocalSettings({ ...localSettings, [id]: value });
};
const handleSave = async () => {
localSettings.jwtTTL =
localSettings.jwtTTLNum + unitItemLookup[localSettings.jwtTTLUnits];
if (hasValidationErrors(localSettings, advancedSettingsValidation, setErrors)) {
return;
}
const action = await dispatch(
updateAppSettings({ settings: localSettings, authToken })
);
let body = "";
if (action.payload.success) {
parseJWTTTL(action.payload.data);
setLocalSettings(action.payload.data);
body = "Settings saved successfully";
} else {
body = "Failed to save settings";
}
createToast({ body });
};
return (
<Box
className="settings"
style={{
paddingBottom: 0,
}}
>
<Stack
component="form"
gap={theme.spacing(12)}
noValidate
spellCheck="false"
>
<ConfigBox>
<Box>
<Typography component="h1">Client settings</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Modify client settings here.
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<TextInput
id="apiBaseUrl"
label="API URL Host"
value={localSettings.apiBaseUrl}
onChange={handleChange}
onBlur={handleBlur}
error={errors.apiBaseUrl ? true : false}
helperText={errors.apiBaseUrl}
/>
<Select
id="logLevel"
label="Log level"
name="logLevel"
items={logItems}
value={logItemLookup[localSettings.logLevel]}
onChange={handleLogLevel}
onBlur={handleBlur}
error={errors.logLevel}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">Email settings</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Set your host email settings here. These settings are used for sending
system emails.
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<TextInput
type="text"
id="systemEmailHost"
label="System email host"
name="systemEmailHost"
value={localSettings.systemEmailHost}
onChange={handleChange}
onBlur={handleBlur}
error={errors.systemEmailHost ? true : false}
helperText={errors.systemEmailHost}
/>
<TextInput
type="number"
id="systemEmailPort"
label="System email port"
name="systemEmailPort"
value={localSettings.systemEmailPort?.toString()}
onChange={handleChange}
onBlur={handleBlur}
error={errors.systemEmailPort ? true : false}
helperText={errors.systemEmailPort}
/>
<TextInput
type="email"
id="systemEmailAddress"
label="System email address"
name="systemEmailAddress"
value={localSettings.systemEmailAddress}
onChange={handleChange}
onBlur={handleBlur}
error={errors.systemEmailAddress ? true : false}
helperText={errors.systemEmailAddress}
/>
<TextInput
type="text"
id="systemEmailPassword"
label="System email password"
name="systemEmailPassword"
value={localSettings.systemEmailPassword}
onChange={handleChange}
onBlur={handleBlur}
error={errors.systemEmailPassword ? true : false}
helperText={errors.systemEmailPassword}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">Server settings</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Modify server settings here.
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Stack
direction="row"
gap={theme.spacing(10)}
>
<TextInput
type="number"
id="jwtTTLNum"
label="JWT time to live"
name="jwtTTLNum"
value={localSettings.jwtTTLNum.toString()}
onChange={handleChange}
onBlur={handleBlur}
error={errors.jwtTTLNum ? true : false}
helperText={errors.jwtTTLNum}
/>
<Select
id="jwtTTLUnits"
label="JWT TTL Units"
name="jwtTTLUnits"
placeholder="Select time"
isHidden={true}
items={unitItems}
value={unitItemLookup[localSettings.jwtTTLUnits]}
onChange={handleJWTTTLUnits}
onBlur={handleBlur}
error={errors.jwtTTLUnits}
/>
</Stack>
<TextInput
type="text"
id="dbType"
label="Database type"
name="dbType"
value={localSettings.dbType}
onChange={handleChange}
onBlur={handleBlur}
error={errors.dbType ? true : false}
helperText={errors.dbType}
/>
<TextInput
type="text"
id="redisHost"
label="Redis host"
name="redisHost"
value={localSettings.redisHost}
onChange={handleChange}
onBlur={handleBlur}
error={errors.redisHost ? true : false}
helperText={errors.redisHost}
/>
<TextInput
type="number"
id="redisPort"
label="Redis port"
name="redisPort"
value={localSettings.redisPort?.toString()}
onChange={handleChange}
onBlur={handleBlur}
error={errors.redisPort ? true : false}
helperText={errors.redisPort}
/>
<TextInput
type="text"
id="pagespeedApiKey"
label="PageSpeed API key"
name="pagespeedApiKey"
value={localSettings.pagespeedApiKey}
onChange={handleChange}
onBlur={handleBlur}
error={errors.pagespeedApiKey ? true : false}
helperText={errors.pagespeedApiKey}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">About</Typography>
</Box>
<Box>
<Typography component="h2">BlueWave Uptime v1.0.0</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}>
Developed by Bluewave Labs.
</Typography>
<Link
level="secondary"
url="https://github.com/bluewave-labs"
label="https://github.com/bluewave-labs"
/>
</Box>
</ConfigBox>
<Stack
direction="row"
justifyContent="flex-end"
>
<LoadingButton
loading={settings.isLoading || settings.authIsLoading}
variant="contained"
color="primary"
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
onClick={handleSave}
>
Save
</LoadingButton>
</Stack>
</Stack>
</Box>
);
};
AdvancedSettings.propTypes = {
isAdmin: PropTypes.bool,
};
export default AdvancedSettings;
+17 -4
View File
@@ -5,10 +5,10 @@ import { useDispatch } from "react-redux";
import { useNavigate } from "react-router";
import { createToast } from "../../Utils/toastUtils";
import { forgotPassword } from "../../Features/Auth/authSlice";
import { IconBox } from "./styled";
import Background from "../../assets/Images/background-grid.svg?react";
import EmailIcon from "../../assets/icons/email.svg?react";
import Logo from "../../assets/icons/bwu-icon.svg?react";
import IconBox from "../../Components/IconBox";
import "./index.css";
const CheckEmail = () => {
@@ -144,9 +144,22 @@ const CheckEmail = () => {
textAlign="center"
>
<Box>
<IconBox>
<EmailIcon alt="email icon" />
</IconBox>
<Stack
direction="row"
justifyContent="center"
>
<IconBox
height={48}
width={48}
minWidth={48}
borderRadius={12}
svgWidth={24}
svgHeight={24}
mb={theme.spacing(4)}
>
<EmailIcon alt="email icon" />
</IconBox>
</Stack>
<Typography component="h1">Check your email</Typography>
<Typography>
We sent a password reset link to{" "}
+17 -4
View File
@@ -6,12 +6,12 @@ import { forgotPassword } from "../../Features/Auth/authSlice";
import { useEffect, useState } from "react";
import { credentials } from "../../Validation/validation";
import { useNavigate } from "react-router-dom";
import { IconBox } from "./styled";
import TextInput from "../../Components/Inputs/TextInput";
import Logo from "../../assets/icons/bwu-icon.svg?react";
import Key from "../../assets/icons/key.svg?react";
import Background from "../../assets/Images/background-grid.svg?react";
import LoadingButton from "@mui/lab/LoadingButton";
import IconBox from "../../Components/IconBox";
import "./index.css";
const ForgotPassword = () => {
@@ -146,9 +146,22 @@ const ForgotPassword = () => {
textAlign="center"
>
<Box>
<IconBox>
<Key alt="password key icon" />
</IconBox>
<Stack
direction="row"
justifyContent="center"
>
<IconBox
height={48}
width={48}
minWidth={48}
borderRadius={12}
svgWidth={24}
svgHeight={24}
mb={theme.spacing(4)}
>
<Key alt="password key icon" />
</IconBox>
</Stack>
<Typography component="h1">Forgot password?</Typography>
<Typography>No worries, we&apos;ll send you reset instructions.</Typography>
</Box>
-562
View File
@@ -1,562 +0,0 @@
import { useState, useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { Box, Button, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { credentials } from "../../Validation/validation";
import { login } from "../../Features/Auth/authSlice";
import LoadingButton from "@mui/lab/LoadingButton";
import { useDispatch, useSelector } from "react-redux";
import { createToast } from "../../Utils/toastUtils";
import { networkService } from "../../main";
import TextInput from "../../Components/Inputs/TextInput";
import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
import Background from "../../assets/Images/background-grid.svg?react";
import Logo from "../../assets/icons/bwu-icon.svg?react";
import Mail from "../../assets/icons/mail.svg?react";
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
import PropTypes from "prop-types";
import { logger } from "../../Utils/Logger";
import "./index.css";
const DEMO = import.meta.env.VITE_APP_DEMO;
/**
* Displays the initial landing page.
*
* @param {Object} props
* @param {Function} props.onContinue - Callback function to handle "Continue with Email" button click.
* @returns {JSX.Element}
*/
const LandingPage = ({ onContinue }) => {
const theme = useTheme();
return (
<>
<Stack
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
alignItems="center"
textAlign="center"
>
<Box>
<Typography component="h1">Log In</Typography>
<Typography>We are pleased to see you again!</Typography>
</Box>
<Box width="100%">
<Button
variant="outlined"
color="info"
onClick={onContinue}
sx={{
width: "100%",
"& svg": {
mr: theme.spacing(4),
"& path": {
stroke: theme.palette.other.icon,
},
},
"&:focus-visible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
},
}}
>
<Mail />
Continue with Email
</Button>
</Box>
<Box maxWidth={400}>
<Typography className="tos-p">
By continuing, you agree to our{" "}
<Typography
component="span"
onClick={() => {
window.open(
"https://bluewavelabs.ca/terms-of-service-open-source",
"_blank",
"noreferrer"
);
}}
sx={{
"&:hover": {
color: theme.palette.text.tertiary,
},
}}
>
Terms of Service
</Typography>{" "}
and{" "}
<Typography
component="span"
onClick={() => {
window.open(
"https://bluewavelabs.ca/privacy-policy-open-source",
"_blank",
"noreferrer"
);
}}
sx={{
"&:hover": {
color: theme.palette.text.tertiary,
},
}}
>
Privacy Policy.
</Typography>
</Typography>
</Box>
</Stack>
</>
);
};
LandingPage.propTypes = {
onContinue: PropTypes.func.isRequired,
};
/**
* Renders the first step of the login process.
*
* @param {Object} props
* @param {Object} props.form - Form state object.
* @param {Object} props.errors - Object containing form validation errors.
* @param {Function} props.onSubmit - Callback function to handle form submission.
* @param {Function} props.onChange - Callback function to handle form input changes.
* @param {Function} props.onBack - Callback function to handle "Back" button click.
* @returns {JSX.Element}
*/
const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
const theme = useTheme();
const inputRef = useRef(null);
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
return (
<>
<Stack
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
textAlign="center"
>
<Box>
<Typography component="h1">Log In</Typography>
<Typography>Enter your email address</Typography>
</Box>
<Box
textAlign="left"
component="form"
noValidate
spellCheck={false}
onSubmit={onSubmit}
display="grid"
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
>
<TextInput
type="email"
id="login-email-input"
label="Email"
isRequired={true}
placeholder="jordan.ellis@domain.com"
autoComplete="email"
value={form.email}
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
onChange={onChange}
error={errors.email ? true : false}
helperText={errors.email}
ref={inputRef}
/>
<Stack
direction="row"
justifyContent="space-between"
>
<Button
variant="outlined"
color="info"
onClick={onBack}
sx={{
px: theme.spacing(5),
"& svg.MuiSvgIcon-root": {
mr: theme.spacing(3),
},
"&:focus-visible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
},
}}
>
<ArrowBackRoundedIcon />
Back
</Button>
<Button
variant="contained"
color="primary"
type="submit"
disabled={errors.email && true}
sx={{
width: "30%",
"&.Mui-focusVisible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
boxShadow: `none`,
},
}}
>
Continue
</Button>
</Stack>
</Box>
</Stack>
</>
);
};
StepOne.propTypes = {
form: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired,
onSubmit: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
};
/**
* Renders the second step of the login process, including a password input field.
*
* @param {Object} props
* @param {Object} props.form - Form state object.
* @param {Object} props.errors - Object containing form validation errors.
* @param {Function} props.onSubmit - Callback function to handle form submission.
* @param {Function} props.onChange - Callback function to handle form input changes.
* @param {Function} props.onBack - Callback function to handle "Back" button click.
* @returns {JSX.Element}
*/
const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
const theme = useTheme();
const navigate = useNavigate();
const inputRef = useRef(null);
const authState = useSelector((state) => state.auth);
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
const handleNavigate = () => {
if (form.email !== "" && !errors.email) {
sessionStorage.setItem("email", form.email);
}
navigate("/forgot-password");
};
return (
<>
<Stack
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
position="relative"
textAlign="center"
>
<Box>
<Typography component="h1">Log In</Typography>
<Typography>Enter your password</Typography>
</Box>
<Box
component="form"
noValidate
spellCheck={false}
onSubmit={onSubmit}
textAlign="left"
mb={theme.spacing(5)}
sx={{
display: "grid",
gap: { xs: theme.spacing(12), sm: theme.spacing(16) },
}}
>
<TextInput
type="password"
id="login-password-input"
label="Password"
isRequired={true}
placeholder="••••••••••"
autoComplete="current-password"
value={form.password}
onChange={onChange}
error={errors.password ? true : false}
helperText={errors.password}
ref={inputRef}
endAdornment={<PasswordEndAdornment />}
/>
<Stack
direction="row"
justifyContent="space-between"
>
<Button
variant="outlined"
color="info"
onClick={onBack}
sx={{
px: theme.spacing(5),
"& svg.MuiSvgIcon-root": {
mr: theme.spacing(3),
},
"&:focus-visible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
},
}}
>
<ArrowBackRoundedIcon />
Back
</Button>
<LoadingButton
variant="contained"
color="primary"
type="submit"
loading={authState.isLoading}
disabled={errors.password && true}
sx={{
width: "30%",
"&.Mui-focusVisible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
boxShadow: `none`,
},
}}
>
Continue
</LoadingButton>
</Stack>
</Box>
<Box
textAlign="center"
sx={{
position: "absolute",
bottom: 0,
left: "50%",
transform: `translate(-50%, 150%)`,
}}
>
<Typography
className="forgot-p"
display="inline-block"
color={theme.palette.primary.main}
>
Forgot password?
</Typography>
<Typography
component="span"
color={theme.palette.primary.main}
ml={theme.spacing(2)}
sx={{ userSelect: "none" }}
onClick={handleNavigate}
>
Reset password
</Typography>
</Box>
</Stack>
</>
);
};
StepTwo.propTypes = {
form: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired,
onSubmit: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
};
const Login = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const theme = useTheme();
const authState = useSelector((state) => state.auth);
const { authToken } = authState;
const idMap = {
"login-email-input": "email",
"login-password-input": "password",
};
const [form, setForm] = useState({
email: DEMO !== undefined ? "uptimedemo@demo.com" : "",
password: DEMO !== undefined ? "Demouser1!" : "",
});
const [errors, setErrors] = useState({});
const [step, setStep] = useState(0);
useEffect(() => {
if (authToken) {
navigate("/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 { error } = credentials.validate({ [name]: value }, { abortEarly: false });
setErrors((prev) => {
const prevErrors = { ...prev };
if (error) prevErrors[name] = error.details[0].message;
else delete prevErrors[name];
return prevErrors;
});
};
const handleSubmit = async (event) => {
event.preventDefault();
if (step === 1) {
const { error } = credentials.validate(
{ email: form.email },
{ abortEarly: false }
);
if (error) {
setErrors((prev) => ({ ...prev, email: error.details[0].message }));
createToast({ body: error.details[0].message });
} else {
setStep(2);
}
} else if (step === 2) {
const { error } = credentials.validate(form, { abortEarly: false });
if (error) {
// validation errors
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
createToast({
body:
error.details && error.details.length > 0
? error.details[0].message
: "Error validating data.",
});
} else {
const action = await dispatch(login(form));
if (action.payload.success) {
navigate("/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>
</Stack>
);
};
export default Login;
@@ -0,0 +1,96 @@
import { useRef, useEffect } from "react";
import { Box, Button, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import TextInput from "../../../../Components/Inputs/TextInput";
import PropTypes from "prop-types";
/**
* Renders the email step of the login process which includes an email field.
*
* @param {Object} props
* @param {Object} props.form - Form state object.
* @param {Object} props.errors - Object containing form validation errors.
* @param {Function} props.onSubmit - Callback function to handle form submission.
* @param {Function} props.onChange - Callback function to handle form input changes.
* @param {Function} props.onBack - Callback function to handle "Back" button click.
* @returns {JSX.Element}
*/
const EmailStep = ({ form, errors, onSubmit, onChange }) => {
const theme = useTheme();
const inputRef = useRef(null);
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
return (
<>
<Stack
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
textAlign="center"
position="relative"
>
<Box>
<Typography component="h1">Log In</Typography>
<Typography>Enter your email address</Typography>
</Box>
<Box
textAlign="left"
component="form"
noValidate
spellCheck={false}
onSubmit={onSubmit}
display="grid"
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
>
<TextInput
type="email"
id="login-email-input"
label="Email"
isRequired={true}
placeholder="jordan.ellis@domain.com"
autoComplete="email"
value={form.email}
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
onChange={onChange}
error={errors.email ? true : false}
helperText={errors.email}
ref={inputRef}
/>
<Stack
direction="row"
justifyContent="flex-end"
>
<Button
variant="contained"
color="primary"
type="submit"
disabled={errors.email && true}
sx={{
width: "30%",
"&.Mui-focusVisible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
boxShadow: `none`,
},
}}
>
Continue
</Button>
</Stack>
</Box>
</Stack>
</>
);
};
EmailStep.propTypes = {
form: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired,
onSubmit: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
};
export default EmailStep;
@@ -0,0 +1,43 @@
import { Box, Typography, useTheme } from "@mui/material";
import PropTypes from "prop-types";
import { useNavigate } from "react-router";
const ForgotPasswordLabel = ({ email, errorEmail }) => {
const theme = useTheme();
const navigate = useNavigate();
const handleNavigate = () => {
if (email !== "" && !errorEmail) {
sessionStorage.setItem("email", email);
}
navigate("/forgot-password");
};
return (
<Box textAlign="center">
<Typography
className="forgot-p"
display="inline-block"
color={theme.palette.primary.main}
>
Forgot password?
</Typography>
<Typography
component="span"
color={theme.palette.primary.main}
ml={theme.spacing(2)}
sx={{ userSelect: "none" }}
onClick={handleNavigate}
>
Reset password
</Typography>
</Box>
);
};
ForgotPasswordLabel.proptype = {
email: PropTypes.string.isRequired,
emailError: PropTypes.string.isRequired,
};
export default ForgotPasswordLabel;
@@ -0,0 +1,124 @@
import { useRef, useEffect } from "react";
import { Box, Button, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import LoadingButton from "@mui/lab/LoadingButton";
import { useSelector } from "react-redux";
import TextInput from "../../../../Components/Inputs/TextInput";
import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments";
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
import PropTypes from "prop-types";
/**
* Renders the password step of the login process, including a password input field.
*
* @param {Object} props
* @param {Object} props.form - Form state object.
* @param {Object} props.errors - Object containing form validation errors.
* @param {Function} props.onSubmit - Callback function to handle form submission.
* @param {Function} props.onChange - Callback function to handle form input changes.
* @param {Function} props.onBack - Callback function to handle "Back" button click.
* @returns {JSX.Element}
*/
const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
const theme = useTheme();
const inputRef = useRef(null);
const authState = useSelector((state) => state.auth);
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
return (
<>
<Stack
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
position="relative"
textAlign="center"
>
<Box>
<Typography component="h1">Log In</Typography>
<Typography>Enter your password</Typography>
</Box>
<Box
component="form"
noValidate
spellCheck={false}
onSubmit={onSubmit}
textAlign="left"
mb={theme.spacing(5)}
sx={{
display: "grid",
gap: { xs: theme.spacing(12), sm: theme.spacing(16) },
}}
>
<TextInput
type="password"
id="login-password-input"
label="Password"
isRequired={true}
placeholder="••••••••••"
autoComplete="current-password"
value={form.password}
onChange={onChange}
error={errors.password ? true : false}
helperText={errors.password}
ref={inputRef}
endAdornment={<PasswordEndAdornment />}
/>
<Stack
direction="row"
justifyContent="space-between"
>
<Button
variant="outlined"
color="info"
onClick={onBack}
sx={{
px: theme.spacing(5),
"& svg.MuiSvgIcon-root": {
mr: theme.spacing(3),
},
"&:focus-visible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
},
}}
>
<ArrowBackRoundedIcon />
Back
</Button>
<LoadingButton
variant="contained"
color="primary"
type="submit"
loading={authState.isLoading}
disabled={errors.password && true}
sx={{
width: "30%",
"&.Mui-focusVisible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
boxShadow: `none`,
},
}}
>
Continue
</LoadingButton>
</Stack>
</Box>
</Stack>
</>
);
};
PasswordStep.propTypes = {
form: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired,
onSubmit: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
};
export default PasswordStep;
+223
View File
@@ -0,0 +1,223 @@
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Box, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { credentials } from "../../../Validation/validation";
import { login } from "../../../Features/Auth/authSlice";
import { useDispatch, useSelector } from "react-redux";
import { createToast } from "../../../Utils/toastUtils";
import { networkService } from "../../../main";
import Background from "../../../assets/Images/background-grid.svg?react";
import Logo from "../../../assets/icons/bwu-icon.svg?react";
import { logger } from "../../../Utils/Logger";
import "../index.css";
import EmailStep from "./Components/EmailStep";
import PasswordStep from "./Components/PasswordStep";
import ThemeSwitch from "../../../Components/ThemeSwitch";
import ForgotPasswordLabel from "./Components/ForgotPasswordLabel";
const DEMO = import.meta.env.VITE_APP_DEMO;
/**
* Displays the login page.
*/
const Login = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const theme = useTheme();
const authState = useSelector((state) => state.auth);
const { authToken } = authState;
const idMap = {
"login-email-input": "email",
"login-password-input": "password",
};
const [form, setForm] = useState({
email: DEMO !== undefined ? "uptimedemo@demo.com" : "",
password: DEMO !== undefined ? "Demouser1!" : "",
});
const [errors, setErrors] = useState({});
const [step, setStep] = useState(0);
useEffect(() => {
if (authToken) {
navigate("/uptime");
return;
}
networkService
.doesSuperAdminExist()
.then((response) => {
if (response.data.data === false) {
navigate("/register");
}
})
.catch((error) => {
logger.error(error);
});
}, [authToken, navigate]);
const handleChange = (event) => {
const { value, id } = event.target;
const name = idMap[id];
setForm((prev) => ({
...prev,
[name]: value,
}));
const { error } = credentials.validate({ [name]: value }, { abortEarly: false });
setErrors((prev) => {
const prevErrors = { ...prev };
if (error) prevErrors[name] = error.details[0].message;
else delete prevErrors[name];
return prevErrors;
});
};
const handleSubmit = async (event) => {
event.preventDefault();
if (step === 0) {
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(1);
}
} else if (step === 1) {
const { error } = credentials.validate(form, { abortEarly: false });
if (error) {
// validation errors
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
createToast({
body:
error.details && error.details.length > 0
? error.details[0].message
: "Error validating data.",
});
} else {
const action = await dispatch(login(form));
if (action.payload.success) {
navigate("/uptime");
createToast({
body: "Welcome back! You're successfully logged in.",
});
} else {
if (action.payload) {
if (action.payload.msg === "Incorrect password")
setErrors({
password: "The password you provided does not match our records",
});
// dispatch errors
createToast({
body: action.payload.msg,
});
} else {
// unknown errors
createToast({
body: "Unknown error.",
});
}
}
}
}
};
return (
<Stack
className="login-page auth"
overflow="hidden"
sx={{
"& h1": {
color: theme.palette.primary.main,
fontWeight: 600,
fontSize: 28,
},
"& p": { fontSize: 14, color: theme.palette.text.accent },
"& span": { fontSize: "inherit" },
}}
>
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.border.light,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
<Stack
direction="row"
alignItems="center"
px={theme.spacing(12)}
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>BlueWave Uptime</Typography>
</Stack>
<Stack
width="100%"
maxWidth={600}
flex={1}
justifyContent="center"
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
pb={theme.spacing(20)}
mx="auto"
rowGap={theme.spacing(8)}
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 ? (
<EmailStep
form={form}
errors={errors}
onSubmit={handleSubmit}
onChange={handleChange}
/>
) : (
step === 1 && (
<PasswordStep
form={form}
errors={errors}
onSubmit={handleSubmit}
onChange={handleChange}
onBack={() => setStep(0)}
/>
)
)}
<ForgotPasswordLabel
email={form.email}
errorEmail={errors.email}
/>
<Box marginX={"auto"}>
<ThemeSwitch />
</Box>
</Stack>
</Stack>
);
};
export default Login;
+18 -5
View File
@@ -4,10 +4,10 @@ import { useNavigate } from "react-router";
import { useDispatch } from "react-redux";
import { clearAuthState } from "../../Features/Auth/authSlice";
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
import { IconBox } from "./styled";
import Background from "../../assets/Images/background-grid.svg?react";
import ConfirmIcon from "../../assets/icons/check-outlined.svg?react";
import Logo from "../../assets/icons/bwu-icon.svg?react";
import IconBox from "../../Components/IconBox";
import "./index.css";
const NewPasswordConfirmed = () => {
@@ -80,9 +80,22 @@ const NewPasswordConfirmed = () => {
textAlign="center"
>
<Box>
<IconBox>
<ConfirmIcon alt="password confirm icon" />
</IconBox>
<Stack
direction="row"
justifyContent="center"
>
<IconBox
height={48}
width={48}
minWidth={48}
borderRadius={12}
svgWidth={24}
svgHeight={24}
mb={theme.spacing(4)}
>
<ConfirmIcon alt="password confirm icon" />
</IconBox>
</Stack>
<Typography component="h1">Password reset</Typography>
<Typography mb={theme.spacing(2)}>
Your password has been successfully reset. Click below to log in magically.
@@ -91,7 +104,7 @@ const NewPasswordConfirmed = () => {
<Button
variant="contained"
color="primary"
onClick={() => navigate("/monitors")}
onClick={() => navigate("/uptime")}
sx={{
width: "100%",
maxWidth: 400,
+1 -1
View File
@@ -237,7 +237,7 @@ const Register = ({ isSuperAdmin }) => {
if (action.payload.success) {
const authToken = action.payload.data;
localStorage.setItem("token", authToken);
navigate("/monitors");
navigate("/uptime");
createToast({
body: "Welcome! Your account was created successfully.",
});
@@ -31,7 +31,6 @@ function StepThree({ onSubmit, onBack }) {
}, []);
const { handleChange, feedbacks, form, errors } = useValidatePassword();
console.log(errors);
return (
<>
<Stack
+17 -5
View File
@@ -11,8 +11,7 @@ import { credentials } from "../../Validation/validation";
import Check from "../../Components/Check/Check";
import TextInput from "../../Components/Inputs/TextInput";
import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
import { IconBox } from "./styled";
import IconBox from "../../Components/IconBox";
import LockIcon from "../../assets/icons/lock.svg?react";
import Logo from "../../assets/icons/bwu-icon.svg?react";
import Background from "../../assets/Images/background-grid.svg?react";
@@ -126,9 +125,22 @@ const SetNewPassword = () => {
textAlign="center"
>
<Box>
<IconBox>
<LockIcon alt="lock icon" />
</IconBox>
<Stack
direction="row"
justifyContent="center"
>
<IconBox
height={48}
width={48}
minWidth={48}
borderRadius={12}
svgWidth={24}
svgHeight={24}
mb={theme.spacing(4)}
>
<LockIcon alt="lock icon" />
</IconBox>
</Stack>
<Typography component="h1">Set new password</Typography>
<Typography>
Your new password must be different to previously used passwords.
-26
View File
@@ -1,26 +0,0 @@
import { Box, styled } from "@mui/material";
export const IconBox = styled(Box)(({ theme }) => ({
height: 48,
minWidth: 48,
width: 48,
position: "relative",
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.dark,
borderRadius: 12,
backgroundColor: theme.palette.background.accent,
margin: "auto",
marginBottom: 8,
"& svg": {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 24,
height: 24,
"& path": {
stroke: theme.palette.text.tertiary,
},
},
}));
@@ -5,32 +5,58 @@ import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
/**
* `CustomThreshold` is a functional React component that displays a
* group of CheckBox with a label and its correspondant threshold input field.
* CustomThreshold Component
*
* @param {{ checkboxId: any; checkboxLabel: any; onCheckboxChange: any; fieldId: any; onFieldChange: any; onFieldBlur: any; alertUnit: any; infrastructureMonitor: any; errors: any; }} param0
* @param {string} param0.checkboxId - The text is the id of the checkbox.
* @param {string} param0.checkboxLabel - The text to be displayed as the label next to the check icon.
* @param {func} param0.onCheckboxChange - The function to invoke when checkbox is checked or unchecked.
* @param {string} param0.fieldId - The text is the id of the input field.
* @param {func} param0.onFieldChange - The function to invoke when input field is changed.
* @param {func} param0.onFieldBlur - The function to invoke when input field is losing focus.
* @param {string} param0.alertUnit the threshold unit such as usage percentage '%' etc
* @param {object} param0.infrastructureMonitor the form object of the create infrastrcuture monitor page
* @param {object} param0.errors the object that holds all the errors of the form page
* @returns A compound React component that renders the custom threshold alert section
* A reusable component that renders a checkbox with an associated numeric input field
* and an optional unit label. The input field can be enabled/disabled based on checkbox state.
*
* @component
* @param {Object} props - Component properties
* @param {string} [props.checkboxId] - Optional unique identifier for the checkbox
* @param {string} [props.checkboxName] - Optional name attribute for the checkbox
* @param {string} props.checkboxLabel - Label text for the checkbox
* @param {boolean} props.isChecked - Current checked state of the checkbox
* @param {Function} props.onCheckboxChange - Callback function when checkbox is toggled
* @param {string} props.fieldId - Unique identifier for the input field
* @param {string} [props.fieldName] - Optional name attribute for the input field
* @param {string} props.fieldValue - Current value of the input field
* @param {Function} props.onFieldChange - Callback function when input field value changes
* @param {Function} props.onFieldBlur - Callback function when input field loses focus
* @param {string} props.alertUnit - Unit label displayed next to the input field
* @param {Object} props.errors - Object containing validation errors for the field
* @param {Object} props.infrastructureMonitor - Infrastructure monitor configuration object
*
* @returns {React.ReactElement} Rendered CustomThreshold component
*
* @example
* <CustomThreshold
* checkboxId="cpu-threshold"
* checkboxName="cpu_threshold"
* checkboxLabel="Enable CPU Threshold"
* isChecked={true}
* onCheckboxChange={handleCheckboxToggle}
* fieldId="cpu-threshold-value"
* fieldName="cpu_threshold_value"
* fieldValue="80"
* onFieldChange={handleFieldChange}
* onFieldBlur={handleFieldBlur}
* alertUnit="%"
* errors={{}}
* infrastructureMonitor={monitorConfig}
* />
*/
export const CustomThreshold = ({
checkboxId,
checkboxName,
checkboxLabel,
onCheckboxChange,
isChecked,
fieldId,
fieldName,
fieldValue,
onFieldChange,
onFieldBlur,
alertUnit,
infrastructureMonitor,
errors,
}) => {
const theme = useTheme();
@@ -46,8 +72,9 @@ export const CustomThreshold = ({
<Box>
<Checkbox
id={checkboxId}
name={checkboxName}
label={checkboxLabel}
isChecked={infrastructureMonitor[checkboxId]}
isChecked={isChecked}
onChange={onCheckboxChange}
/>
</Box>
@@ -61,11 +88,12 @@ export const CustomThreshold = ({
maxWidth="var(--env-var-width-4)"
type="number"
id={fieldId}
value={infrastructureMonitor[fieldId]}
name={fieldName}
value={fieldValue}
onBlur={onFieldBlur}
onChange={onFieldChange}
error={errors[fieldId] ? true : false}
disabled={!infrastructureMonitor[checkboxId]}
disabled={!isChecked}
/>
<Typography
@@ -80,10 +108,14 @@ export const CustomThreshold = ({
};
CustomThreshold.propTypes = {
checkboxId: PropTypes.string.isRequired,
checkboxId: PropTypes.string,
checkboxName: PropTypes.string,
checkboxLabel: PropTypes.string.isRequired,
isChecked: PropTypes.bool.isRequired,
onCheckboxChange: PropTypes.func.isRequired,
fieldId: PropTypes.string.isRequired,
fieldName: PropTypes.string,
fieldValue: PropTypes.string.isRequired,
onFieldChange: PropTypes.func.isRequired,
onFieldBlur: PropTypes.func.isRequired,
alertUnit: PropTypes.string.isRequired,
@@ -1,27 +1,62 @@
import { useState } from "react";
import { Box, Stack, Typography } from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton";
import { useSelector, useDispatch } from "react-redux";
import { infrastructureMonitorValidation } from "../../../Validation/validation";
import { parseDomainName } from "../../../Utils/monitorUtils";
import {
createInfrastructureMonitor,
checkInfrastructureEndpointResolution,
} from "../../../Features/InfrastructureMonitors/infrastructureMonitorsSlice";
import { useNavigate } from "react-router-dom";
// React, Redux, Router
import { useTheme } from "@emotion/react";
import { createToast } from "../../../Utils/toastUtils";
import Link from "../../../Components/Link";
import { ConfigBox } from "../../Monitors/styled";
import TextInput from "../../../Components/Inputs/TextInput";
import Select from "../../../Components/Inputs/Select";
import Checkbox from "../../../Components/Inputs/Checkbox";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import { buildErrors, hasValidationErrors } from "../../../Validation/error";
import { useNavigate } from "react-router-dom";
import { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
// Utility and Network
import { infrastructureMonitorValidation } from "../../../Validation/validation";
import { createInfrastructureMonitor } from "../../../Features/InfrastructureMonitors/infrastructureMonitorsSlice";
import { capitalizeFirstLetter } from "../../../Utils/stringUtils";
import { CustomThreshold } from "../CreateMonitor/CustomThreshold";
// MUI
import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton";
//Components
import Breadcrumbs from "../../../Components/Breadcrumbs";
import Link from "../../../Components/Link";
import ConfigBox from "../../../Components/ConfigBox";
import TextInput from "../../../Components/Inputs/TextInput";
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
import { createToast } from "../../../Utils/toastUtils";
import Checkbox from "../../../Components/Inputs/Checkbox";
import Select from "../../../Components/Inputs/Select";
import { CustomThreshold } from "./CustomThreshold";
const SELECT_VALUES = [
{ _id: 0.25, name: "15 seconds" },
{ _id: 0.5, name: "30 seconds" },
{ _id: 1, name: "1 minute" },
{ _id: 2, name: "2 minutes" },
{ _id: 5, name: "5 minutes" },
{ _id: 10, name: "10 minutes" },
];
const METRICS = ["cpu", "memory", "disk", "temperature"];
const METRIC_PREFIX = "usage_";
const MS_PER_MINUTE = 60000;
const hasAlertError = (errors) => {
return Object.keys(errors).filter((k) => k.startsWith(METRIC_PREFIX)).length > 0;
};
const getAlertError = (errors) => {
return Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX))
? errors[Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX))]
: null;
};
const CreateInfrastructureMonitor = () => {
const theme = useTheme();
const { user, authToken } = useSelector((state) => state.auth);
const monitorState = useSelector((state) => state.infrastructureMonitor);
const dispatch = useDispatch();
const navigate = useNavigate();
// State
const [errors, setErrors] = useState({});
const [https, setHttps] = useState(false);
const [infrastructureMonitor, setInfrastructureMonitor] = useState({
url: "",
name: "",
@@ -38,118 +73,73 @@ const CreateInfrastructureMonitor = () => {
secret: "",
});
const MS_PER_MINUTE = 60000;
const THRESHOLD_FIELD_PREFIX = "usage_";
const HARDWARE_MONITOR_TYPES = ["cpu", "memory", "disk", "temperature"];
const { user, authToken } = useSelector((state) => state.auth);
const monitorState = useSelector((state) => state.infrastructureMonitor);
const dispatch = useDispatch();
const navigate = useNavigate();
const theme = useTheme();
const idMap = {
"notify-email-default": "notification-email",
};
const [errors, setErrors] = useState({});
const alertErrKeyLen = Object.keys(errors).filter((k) =>
k.startsWith(THRESHOLD_FIELD_PREFIX)
).length;
const handleCustomAlertCheckChange = (event) => {
const { value, id } = event.target;
setInfrastructureMonitor((prev) => {
const newState = {
[id]: prev[id] == undefined && value == "on" ? true : !prev[id],
};
return {
...prev,
...newState,
[THRESHOLD_FIELD_PREFIX + id]: newState[id]
? prev[THRESHOLD_FIELD_PREFIX + id]
: "",
};
});
// Remove the error if unchecked
setErrors((prev) => {
return buildErrors(prev, [THRESHOLD_FIELD_PREFIX + id]);
});
};
const handleBlur = (event, appendID) => {
// Handlers
const handleCreateInfrastructureMonitor = async (event) => {
event.preventDefault();
const { value, id } = event.target;
let name = idMap[id] ?? id;
if (name === "url" && infrastructureMonitor.name === "") {
setInfrastructureMonitor((prev) => ({
...prev,
name: parseDomainName(value),
}));
// Build the form
let form = {
url: `http${https ? "s" : ""}://` + infrastructureMonitor.url,
name:
infrastructureMonitor.name === ""
? infrastructureMonitor.url
: infrastructureMonitor.name,
interval: infrastructureMonitor.interval * MS_PER_MINUTE,
cpu: infrastructureMonitor.cpu,
...(infrastructureMonitor.cpu
? { usage_cpu: infrastructureMonitor.usage_cpu }
: {}),
memory: infrastructureMonitor.memory,
...(infrastructureMonitor.memory
? { usage_memory: infrastructureMonitor.usage_memory }
: {}),
disk: infrastructureMonitor.disk,
...(infrastructureMonitor.disk
? { usage_disk: infrastructureMonitor.usage_disk }
: {}),
temperature: infrastructureMonitor.temperature,
...(infrastructureMonitor.temperature
? { usage_temperature: infrastructureMonitor.usage_temperature }
: {}),
secret: infrastructureMonitor.secret,
};
const { error } = infrastructureMonitorValidation.validate(form, {
abortEarly: false,
});
if (error) {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
createToast({ body: "Please check the form for errors." });
return;
}
if (id?.startsWith("notify-email-")) return;
const { error } = infrastructureMonitorValidation.validate(
{ [id ?? appendID]: value },
{
abortEarly: false,
}
);
setErrors((prev) => {
return buildErrors(prev, id ?? appendID, error);
});
};
// Build the thresholds for the form
const {
cpu,
usage_cpu,
memory,
usage_memory,
disk,
usage_disk,
temperature,
usage_temperature,
...rest
} = form;
const handleChange = (event, appendedId) => {
event.preventDefault();
const { value, id } = event.target;
let name = appendedId ?? idMap[id] ?? id;
if (name.includes("notification-")) {
name = name.replace("notification-", "");
let hasNotif = infrastructureMonitor.notifications.some(
(notification) => notification.type === name
);
setInfrastructureMonitor((prev) => {
const notifs = [...prev.notifications];
if (hasNotif) {
return {
...prev,
notifications: notifs.filter((notif) => notif.type !== name),
};
} else {
return {
...prev,
notifications: [
...notifs,
name === "email"
? { type: name, address: value }
: // TODO - phone number
{ type: name, phone: value },
],
};
}
});
} else {
setInfrastructureMonitor((prev) => ({
...prev,
[name]: value,
}));
}
};
const generatePayload = (form) => {
let thresholds = {};
Object.keys(form)
.filter((k) => k.startsWith(THRESHOLD_FIELD_PREFIX))
.map((k) => {
if (form[k]) thresholds[k] = form[k] / 100;
delete form[k];
delete form[k.substring(THRESHOLD_FIELD_PREFIX.length)];
});
const thresholds = {
...(cpu ? { usage_cpu: usage_cpu / 100 } : {}),
...(memory ? { usage_memory: usage_memory / 100 } : {}),
...(disk ? { usage_disk: usage_disk / 100 } : {}),
...(temperature ? { usage_temperature: usage_temperature / 100 } : {}),
};
form = {
...form,
...rest,
description: form.name,
teamId: user.teamId,
userId: user._id,
@@ -157,61 +147,76 @@ const CreateInfrastructureMonitor = () => {
notifications: infrastructureMonitor.notifications,
thresholds,
};
return form;
};
const handleCreateInfrastructureMonitor = async (event) => {
event.preventDefault();
let form = {
...infrastructureMonitor,
name:
infrastructureMonitor.name === ""
? infrastructureMonitor.url
: infrastructureMonitor.name,
interval: infrastructureMonitor.interval * MS_PER_MINUTE,
};
delete form.notifications;
if (hasValidationErrors(form, infrastructureMonitorValidation, setErrors)) {
return;
const action = await dispatch(
createInfrastructureMonitor({ authToken, monitor: form })
);
if (action.meta.requestStatus === "fulfilled") {
createToast({ body: "Infrastructure monitor created successfully!" });
navigate("/infrastructure");
} else {
const checkEndpointAction = await dispatch(
checkInfrastructureEndpointResolution({ authToken, monitorURL: form.url })
);
if (checkEndpointAction.meta.requestStatus === "rejected") {
createToast({
body: "The endpoint you entered doesn't resolve. Check the URL again.",
});
setErrors({ url: "The entered URL is not reachable." });
return;
}
const action = await dispatch(
createInfrastructureMonitor({ authToken, monitor: generatePayload(form) })
);
if (action.meta.requestStatus === "fulfilled") {
createToast({ body: "Infrastructure monitor created successfully!" });
navigate("/infrastructure");
} else {
createToast({ body: "Failed to create monitor." });
}
createToast({ body: "Failed to create monitor." });
}
};
//select values
const frequencies = [
{ _id: 0.25, name: "15 seconds" },
{ _id: 0.5, name: "30 seconds" },
{ _id: 1, name: "1 minute" },
{ _id: 2, name: "2 minutes" },
{ _id: 5, name: "5 minutes" },
{ _id: 10, name: "10 minutes" },
];
const handleChange = (event) => {
const { value, name } = event.target;
setInfrastructureMonitor({
...infrastructureMonitor,
[name]: value,
});
const { error } = infrastructureMonitorValidation.validate(
{ [name]: value },
{ abortEarly: false }
);
setErrors((prev) => ({
...prev,
...(error ? { [name]: error.details[0].message } : { [name]: undefined }),
}));
};
const handleCheckboxChange = (event) => {
const { name } = event.target;
const { checked } = event.target;
setInfrastructureMonitor({
...infrastructureMonitor,
[name]: checked,
});
};
const handleNotifications = (event, type) => {
const { value } = event.target;
let notifications = [...infrastructureMonitor.notifications];
const notificationExists = notifications.some((notification) => {
if (notification.type === type && notification.address === value) {
return true;
}
return false;
});
if (notificationExists) {
notifications = notifications.filter((notification) => {
if (notification.type === type && notification.address === value) {
return false;
}
return true;
});
} else {
notifications.push({ type, address: value });
}
setInfrastructureMonitor((prev) => ({
...prev,
notifications,
}));
};
return (
<Box className="create-infrastructure-monitor">
<Breadcrumbs
list={[
{ name: "Infrastructure monitors", path: "infrastructure" },
{ name: "create", path: `infrastructure/create` },
{ name: "Infrastructure monitors", path: "/infrastructure" },
{ name: "create", path: `/infrastructure/create` },
]}
/>
<Stack
@@ -239,56 +244,75 @@ const CreateInfrastructureMonitor = () => {
fontSize="inherit"
fontWeight="inherit"
>
infrastructure monitor
monitor
</Typography>
</Typography>
<ConfigBox>
<Box>
<Stack gap={theme.spacing(6)}>
<Typography component="h2">General settings</Typography>
<Typography component="p">
Here you can select the URL of the host, together with the friendly name
and authorization secret to connect to the server agent.
</Typography>
<Typography component="p">
The server you are monitoring must be running the{" "}
<Link
level="primary"
url="https://github.com/bluewave-labs/checkmate-agent"
label="Checkmate Monitoring Agent"
/>
</Typography>
</Stack>
</Box>
<Stack gap={theme.spacing(6)}>
<Typography component="h2">General settings</Typography>
<Typography component="p">
Here you can select the URL of the host, together with the friendly name and
authorization secret to connect to the server agent.
</Typography>
<Typography component="p">
The server you are monitoring must be running the{" "}
<Link
level="primary"
url="https://github.com/bluewave-labs/checkmate-agent"
label="Checkmate Monitoring Agent"
/>
</Typography>
</Stack>
<Stack gap={theme.spacing(15)}>
<TextInput
type="text"
type="url"
id="url"
name="url"
startAdornment={<HttpAdornment https={https} />}
placeholder={"localhost:59232/api/v1/metrics"}
label="Server URL"
placeholder="https://"
https={https}
value={infrastructureMonitor.url}
onBlur={handleBlur}
onChange={handleChange}
error={errors["url"] ? true : false}
helperText={errors["url"]}
/>
<Box>
<Typography component="p">Protocol</Typography>
<ButtonGroup>
<Button
variant="group"
filled={https.toString()}
onClick={() => setHttps(true)}
>
HTTPS
</Button>
<Button
variant="group"
filled={(!https).toString()}
onClick={() => setHttps(false)}
>
HTTP
</Button>
</ButtonGroup>
</Box>
<TextInput
type="text"
id="name"
name="name"
label="Display name"
placeholder="Google"
isOptional={true}
value={infrastructureMonitor.name}
onBlur={handleBlur}
onChange={handleChange}
error={errors["name"]}
/>
<TextInput
type="text"
id="secret"
name="secret"
label="Authorization secret"
value={infrastructureMonitor.secret}
onBlur={handleBlur}
onChange={handleChange}
error={errors["secret"] ? true : false}
helperText={errors["secret"]}
@@ -303,7 +327,6 @@ const CreateInfrastructureMonitor = () => {
</Typography>
</Box>
<Stack gap={theme.spacing(6)}>
<Typography component="p">When there is a new incident,</Typography>
<Checkbox
id="notify-email-default"
label={`Notify via email (to ${user.email})`}
@@ -311,12 +334,10 @@ const CreateInfrastructureMonitor = () => {
(notification) => notification.type === "email"
)}
value={user?.email}
onChange={(e) => handleChange(e)}
onBlur={handleBlur}
onChange={(event) => handleNotifications(event, "email")}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">Customize alerts</Typography>
@@ -326,25 +347,31 @@ const CreateInfrastructureMonitor = () => {
</Typography>
</Box>
<Stack gap={theme.spacing(6)}>
{HARDWARE_MONITOR_TYPES.map((type, idx) => (
<CustomThreshold
key={idx}
checkboxId={type}
checkboxLabel={
type !== "cpu" ? capitalizeFirstLetter(type) : type.toUpperCase()
}
onCheckboxChange={handleCustomAlertCheckChange}
fieldId={THRESHOLD_FIELD_PREFIX + type}
fieldValue={infrastructureMonitor[THRESHOLD_FIELD_PREFIX + type] ?? ""}
onFieldChange={handleChange}
onFieldBlur={handleBlur}
// TODO: need BE, maybe in another PR
alertUnit={type == "temperature" ? "°C" : "%"}
infrastructureMonitor={infrastructureMonitor}
errors={errors}
/>
))}
{alertErrKeyLen > 0 && (
{METRICS.map((metric) => {
return (
<CustomThreshold
key={metric}
infrastructureMonitor={infrastructureMonitor}
errors={errors}
checkboxId={metric}
checkboxName={metric}
checkboxLabel={
metric !== "cpu"
? capitalizeFirstLetter(metric)
: metric.toUpperCase()
}
onCheckboxChange={handleCheckboxChange}
isChecked={infrastructureMonitor[metric]}
fieldId={METRIC_PREFIX + metric}
fieldName={METRIC_PREFIX + metric}
fieldValue={infrastructureMonitor[METRIC_PREFIX + metric]}
onFieldChange={handleChange}
alertUnit={metric == "temperature" ? "°C" : "%"}
/>
);
})}
{/* Error text */}
{hasAlertError(errors) && (
<Typography
component="span"
className="input-error"
@@ -354,14 +381,7 @@ const CreateInfrastructureMonitor = () => {
opacity: 0.8,
}}
>
{
errors[
THRESHOLD_FIELD_PREFIX +
HARDWARE_MONITOR_TYPES.filter(
(type) => errors[THRESHOLD_FIELD_PREFIX + type]
)[0]
]
}
{getAlertError(errors)}
</Typography>
)}
</Stack>
@@ -373,11 +393,11 @@ const CreateInfrastructureMonitor = () => {
<Stack gap={theme.spacing(12)}>
<Select
id="interval"
name="interval"
label="Check frequency"
value={infrastructureMonitor.interval || 15}
onChange={(e) => handleChange(e, "interval")}
onBlur={(e) => handleBlur(e, "interval")}
items={frequencies}
onChange={handleChange}
items={SELECT_VALUES}
/>
</Stack>
</ConfigBox>
@@ -21,8 +21,7 @@ const Empty = ({ styles }) => {
textAlign="center"
color={theme.palette.text.secondary}
>
Hang tight! When we receive data, we'll show it here. Please check back in a few
minutes.
Hang tight! Data is loading
</Typography>
</Stack>
</Box>
@@ -8,7 +8,7 @@ import AreaChart from "../../../Components/Charts/AreaChart";
import { useSelector } from "react-redux";
import { networkService } from "../../../main";
import PulseDot from "../../../Components/Animated/PulseDot";
import useUtils from "../../Monitors/utils";
import useUtils from "../../Uptime/utils";
import { useNavigate } from "react-router-dom";
import Empty from "./empty";
import { logger } from "../../../Utils/Logger";
@@ -20,6 +20,7 @@ import {
TemperatureTooltip,
} from "../../../Components/Charts/Utils/chartUtils";
import PropTypes from "prop-types";
import StatBox from "../../../Components/StatBox";
const BASE_BOX_PADDING_VERTICAL = 4;
const BASE_BOX_PADDING_HORIZONTAL = 8;
@@ -29,18 +30,50 @@ const TYPOGRAPHY_PADDING = 8;
* @param {number} bytes - Number of bytes to convert
* @returns {number} Converted value in gigabytes
*/
const formatBytes = (bytes) => {
if (bytes === undefined || bytes === null) return "0 GB";
if (typeof bytes !== "number") return "0 GB";
if (bytes === 0) return "0 GB";
const formatBytes = (bytes, space = false) => {
if (bytes === undefined || bytes === null)
return (
<>
{0}
{space ? " " : ""}
<Typography component="span">GB</Typography>
</>
);
if (typeof bytes !== "number")
return (
<>
{0}
{space ? " " : ""}
<Typography component="span">GB</Typography>
</>
);
if (bytes === 0)
return (
<>
{0}
{space ? " " : ""}
<Typography component="span">GB</Typography>
</>
);
const GB = bytes / (1024 * 1024 * 1024);
const MB = bytes / (1024 * 1024);
if (GB >= 1) {
return `${Number(GB.toFixed(0))} GB`;
return (
<>
{Number(GB.toFixed(0))}
{space ? " " : ""}
<Typography component="span">GB</Typography>
</>
);
} else {
return `${Number(MB.toFixed(0))} MB`;
return (
<>
{Number(MB.toFixed(0))}
<Typography component="span">MB</Typography>
</>
);
}
};
@@ -93,27 +126,6 @@ BaseBox.propTypes = {
sx: PropTypes.object,
};
/**
* Renders a statistic box with a heading and subheading
* @param {Object} props - Component properties
* @param {string} props.heading - Primary heading text
* @param {string} props.subHeading - Secondary heading text
* @returns {React.ReactElement} Stat box component
*/
const StatBox = ({ heading, subHeading }) => {
return (
<BaseBox>
<Typography component="h2">{heading}</Typography>
<Typography>{subHeading}</Typography>
</BaseBox>
);
};
StatBox.propTypes = {
heading: PropTypes.string.isRequired,
subHeading: PropTypes.string.isRequired,
};
/**
* Renders a gauge box with usage visualization
* @param {Object} props - Component properties
@@ -193,7 +205,7 @@ const InfrastructureDetails = () => {
const [monitor, setMonitor] = useState(null);
const { authToken } = useSelector((state) => state.auth);
const [dateRange, setDateRange] = useState("all");
const { statusColor, determineState } = useUtils();
const { statusColor, statusStyles, determineState } = useUtils();
// These calculations are needed because ResponsiveContainer
// doesn't take padding of parent/siblings into account
// when calculating height.
@@ -205,7 +217,7 @@ const InfrastructureDetails = () => {
(chartContainerHeight - totalChartContainerPadding - totalTypographyPadding) * 0.95;
// end height calculations
const buildStatBoxes = (checks) => {
const buildStatBoxes = (checks, uptime) => {
let latestCheck = checks[0] ?? null;
if (latestCheck === null) return [];
@@ -224,25 +236,51 @@ const InfrastructureDetails = () => {
const platform = latestCheck?.host?.platform ?? null;
const osPlatform = os === null && platform === null ? null : `${os} ${platform}`;
return [
{
id: 7,
sx: statusStyles[determineState(monitor)],
heading: "Status",
subHeading: monitor?.status === true ? "Active" : "Inactive",
},
{
id: 0,
heading: "CPU (Physical)",
subHeading: `${physicalCores} cores`,
subHeading: (
<>
{physicalCores}
<Typography component="span">cores</Typography>
</>
),
},
{
id: 1,
heading: "CPU (Logical)",
subHeading: `${logicalCores} cores`,
subHeading: (
<>
{logicalCores}
<Typography component="span">cores</Typography>
</>
),
},
{
id: 2,
heading: "CPU Frequency",
subHeading: `${(cpuFrequency / 1000).toFixed(2)} Ghz`,
subHeading: (
<>
{(cpuFrequency / 1000).toFixed(2)}
<Typography component="span">Ghz</Typography>
</>
),
},
{
id: 3,
heading: "Average CPU Temperature",
subHeading: `${cpuTemperature.toFixed(2)} C`,
subHeading: (
<>
{cpuTemperature.toFixed(2)}
<Typography component="span">C</Typography>
</>
),
},
{
id: 4,
@@ -254,12 +292,17 @@ const InfrastructureDetails = () => {
heading: "Disk",
subHeading: formatBytes(diskTotalBytes),
},
{ id: 6, heading: "Uptime", subHeading: "100%" },
{
id: 7,
heading: "Status",
subHeading: monitor?.status === true ? "Active" : "Inactive",
id: 6,
heading: "Uptime",
subHeading: (
<>
{(uptime * 100).toFixed(2)}
<Typography component="span">%</Typography>
</>
),
},
{
id: 8,
heading: "OS",
@@ -283,16 +326,16 @@ const InfrastructureDetails = () => {
{
type: "memory",
value: decimalToPercentage(memoryUsagePercent),
heading: "Memory Usage",
heading: "Memory usage",
metricOne: "Used",
valueOne: formatBytes(memoryUsedBytes),
valueOne: formatBytes(memoryUsedBytes, true),
metricTwo: "Total",
valueTwo: formatBytes(memoryTotalBytes),
valueTwo: formatBytes(memoryTotalBytes, true),
},
{
type: "cpu",
value: decimalToPercentage(cpuUsagePercent),
heading: "CPU Usage",
heading: "CPU usage",
metricOne: "Cores",
valueOne: cpuPhysicalCores ?? 0,
metricTwo: "Frequency",
@@ -304,9 +347,9 @@ const InfrastructureDetails = () => {
value: decimalToPercentage(disk.usage_percent),
heading: `Disk${idx} usage`,
metricOne: "Used",
valueOne: formatBytes(disk.total_bytes - disk.free_bytes),
valueOne: formatBytes(disk.total_bytes - disk.free_bytes, true),
metricTwo: "Total",
valueTwo: formatBytes(disk.total_bytes),
valueTwo: formatBytes(disk.total_bytes, true),
})),
];
};
@@ -364,7 +407,7 @@ const InfrastructureDetails = () => {
heading: "Memory usage",
strokeColor: theme.palette.primary.main,
gradientStartColor: theme.palette.primary.main,
yLabel: "Memory Usage",
yLabel: "Memory usage",
yDomain: [0, 1],
yTick: <PercentTick />,
xTick: <TzTick />,
@@ -372,7 +415,7 @@ const InfrastructureDetails = () => {
<InfrastructureTooltip
dotColor={theme.palette.primary.main}
yKey={"memory.usage_percent"}
yLabel={"Memory Usage"}
yLabel={"Memory usage"}
/>
),
},
@@ -383,7 +426,7 @@ const InfrastructureDetails = () => {
heading: "CPU usage",
strokeColor: theme.palette.success.main,
gradientStartColor: theme.palette.success.main,
yLabel: "CPU Usage",
yLabel: "CPU usage",
yDomain: [0, 1],
yTick: <PercentTick />,
xTick: <TzTick />,
@@ -391,7 +434,7 @@ const InfrastructureDetails = () => {
<InfrastructureTooltip
dotColor={theme.palette.success.main}
yKey={"cpu.usage_percent"}
yLabel={"CPU Usage"}
yLabel={"CPU usage"}
/>
),
},
@@ -466,7 +509,10 @@ const InfrastructureDetails = () => {
fetchData();
}, [authToken, monitorId, dateRange, navigate]);
const statBoxConfigs = buildStatBoxes(monitor?.checks ?? []);
const statBoxConfigs = buildStatBoxes(
monitor?.checks ?? [],
monitor?.uptimePercentage ?? "Unknown"
);
const gaugeBoxConfigs = buildGaugeBoxConfigs(monitor?.checks ?? []);
const areaChartConfigs = buildAreaChartConfigs(monitor?.checks ?? []);
@@ -109,88 +109,6 @@ const InfrastructureMenu = ({ monitor, isAdmin, updateCallback }) => {
},
}}
>
{/*
Open site action. Not necessary for infrastructure?
{actions.url !== null ? (
<MenuItem
onClick={(e) => {
closeMenu(e);
e.stopPropagation();
window.open(actions.url, "_blank", "noreferrer");
}}
>
Open site
</MenuItem>
) : (
""
)}
*/}
<MenuItem onClick={() => openDetails(monitor.id)}>Details</MenuItem>
{/*
Incidents. Necessary?
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/incidents/${actions.id}`);
}}
>
Incidents
</MenuItem> */}
{/*
Configure. Necessary?
{isAdmin && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/monitors/configure/${actions.id}`);
}}
>
Configure
</MenuItem>
)} */}
{/*
Clone. Necessary?
{isAdmin && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/monitors/create/${actions.id}`);
}}
>
Clone
</MenuItem>
)} */}
{/*
Pause. Necessary?
const handlePause = async () => {
try {
const action = await dispatch(
pauseUptimeMonitor({ authToken, monitorId: monitor._id })
);
if (pauseUptimeMonitor.fulfilled.match(action)) {
updateCallback();
const state = action?.payload?.data.isActive === false ? "paused" : "resumed";
createToast({ body: `Monitor ${state} successfully.` });
} else {
throw new Error(action?.error?.message ?? "Failed to pause monitor.");
}
} catch (error) {
logger.error("Error pausing monitor:", monitor._id, error);
createToast({ body: "Failed to pause monitor." });
}
};
{isAdmin && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
handlePause(e);
}}
>
{monitor?.isActive === true ? "Pause" : "Resume"}
</MenuItem>
)} */}
{isAdmin && <MenuItem onClick={openRemove}>Remove</MenuItem>}
</Menu>
<Dialog
+21 -26
View File
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { /* useDispatch, */ useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import useUtils from "../Monitors/utils";
import useUtils from "../Uptime/utils.jsx";
import { jwtDecode } from "jwt-decode";
import SkeletonLayout from "./skeleton";
import Fallback from "../../Components/Fallback";
@@ -28,7 +28,7 @@ import { Pagination } from "./components/TablePagination";
// import { getInfrastructureMonitorsByTeamId } from "../../Features/InfrastructureMonitors/infrastructureMonitorsSlice";
import { networkService } from "../../Utils/NetworkService.js";
import CustomGauge from "../../Components/Charts/CustomGauge/index.jsx";
import Host from "../Monitors/Home/host.jsx";
import Host from "../Uptime/Home/host.jsx";
import { useIsAdmin } from "../../Hooks/useIsAdmin.js";
import { InfrastructureMenu } from "./components/Menu";
@@ -191,31 +191,26 @@ function Infrastructure() {
<SkeletonLayout />
) : monitorState.monitors?.length !== 0 ? (
<Stack gap={theme.spacing(8)}>
<Breadcrumbs list={BREADCRUMBS} />
<Stack
direction="row"
sx={{
justifyContent: "end",
alignItems: "center",
gap: "1rem",
flexWrap: "wrap",
marginBottom: "2rem",
}}
>
{/*
This will be removed from here, but keeping the commented code to remind me to add a max width to the greeting component
<Box style={{ maxWidth: "65ch" }}>
<Greeting type="uptime" />
</Box> */}
<Button
variant="contained"
color="primary"
onClick={navigateToCreate}
sx={{ fontWeight: 500 }}
<Box>
<Breadcrumbs list={BREADCRUMBS} />
<Stack
direction="row"
justifyContent="end"
alignItems="center"
mt={theme.spacing(5)}
>
Create infrastructure monitor
</Button>
</Stack>
{isAdmin && (
<Button
variant="contained"
color="primary"
onClick={navigateToCreate}
sx={{ fontWeight: 500, whiteSpace: "nowrap" }}
>
Create new
</Button>
)}
</Stack>
</Box>
<Stack
sx={{
gap: "1rem",
@@ -2,7 +2,7 @@ import { Box, Button, Stack, Typography } from "@mui/material";
import { useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import { useEffect, useState } from "react";
import { ConfigBox } from "./styled";
import ConfigBox from "../../../Components/ConfigBox";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
@@ -321,18 +321,27 @@ const CreateMaintenance = () => {
Your pings won&apos;t be sent during this time frame
</Typography>
</Box>
<ConfigBox direction="row">
<Typography
component="h2"
variant="h2"
>
General Settings
</Typography>
<Box
px={theme.spacing(14)}
borderLeft={1}
borderLeftColor={theme.palette.border.light}
>
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
General Settings
</Typography>
</Box>
<Stack gap={theme.spacing(15)}>
<TextInput
id="name"
label="Friendly name"
placeholder="Maintenance at __ : __ for ___ minutes"
value={form.name}
onChange={(event) => {
handleFormChange("name", event.target.value);
}}
error={errors["name"] ? true : false}
helperText={errors["name"]}
/>
<Select
id="repeat"
name="maintenance-repeat"
@@ -346,15 +355,12 @@ const CreateMaintenance = () => {
}}
items={repeatConfig}
/>
<Stack
gap={theme.spacing(2)}
mt={theme.spacing(16)}
>
<Typography component="h3">Date</Typography>
<Stack>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
id="startDate"
disablePast
label="Date"
disableHighlightToday
value={form.startDate}
slots={{ openPickerIcon: CalendarIcon }}
@@ -403,72 +409,64 @@ const CreateMaintenance = () => {
/>
</LocalizationProvider>
</Stack>
</Box>
</Stack>
</ConfigBox>
<ConfigBox>
<Stack direction="row">
<Box>
<Typography
component="h2"
variant="h2"
>
Start time
</Typography>
<Typography>All dates and times are in GMT+0 time zone.</Typography>
</Box>
<Stack direction="row">
<LocalizationProvider dateAdapter={AdapterDayjs}>
<MobileTimePicker
id="startTime"
value={form.startTime}
onChange={(newTime) => {
handleTimeChange("startTime", newTime);
}}
slotProps={{
nextIconButton: { sx: { ml: theme.spacing(2) } },
field: {
sx: {
width: "fit-content",
"& > .MuiOutlinedInput-root": {
flexDirection: "row-reverse",
},
"& input": {
height: 34,
p: 0,
pl: theme.spacing(5),
},
"& fieldset": {
borderColor: theme.palette.border.dark,
borderRadius: theme.shape.borderRadius,
},
"&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset":
{
borderColor: theme.palette.border.dark,
},
<Box>
<Typography
component="h2"
variant="h2"
>
Start time
</Typography>
<Typography>All dates and times are in GMT+0 time zone.</Typography>
</Box>
<Stack gap={theme.spacing(15)}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<MobileTimePicker
id="startTime"
label="Start time"
value={form.startTime}
onChange={(newTime) => {
handleTimeChange("startTime", newTime);
}}
slotProps={{
nextIconButton: { sx: { ml: theme.spacing(2) } },
field: {
sx: {
width: "fit-content",
"& > .MuiOutlinedInput-root": {
flexDirection: "row-reverse",
},
"& input": {
height: 34,
p: 0,
pl: theme.spacing(5),
},
"& fieldset": {
borderColor: theme.palette.border.dark,
borderRadius: theme.shape.borderRadius,
},
"&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset":
{
borderColor: theme.palette.border.dark,
},
},
}}
error={errors["startTime"]}
/>
</LocalizationProvider>
</Stack>
</Stack>
<Stack direction="row">
<Box>
<Typography
component="h2"
variant="h2"
>
Duration
</Typography>
</Box>
},
}}
error={errors["startTime"]}
/>
</LocalizationProvider>
<Stack
direction="row"
alignItems="end"
spacing={theme.spacing(8)}
>
<TextInput
type="number"
id="duration"
label="Duration"
value={form.duration}
onChange={(event) => {
handleFormChange("duration", event.target.value);
@@ -491,66 +489,34 @@ const CreateMaintenance = () => {
</Stack>
</Stack>
</ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
fontSize={16}
my={theme.spacing(6)}
>
Monitor related settings
</Typography>
<ConfigBox>
<Stack direction="row">
<Box>
<Typography
component="h2"
variant="h2"
>
Friendly name
</Typography>
</Box>
<Box>
<TextInput
id="name"
placeholder="Maintenance at __ : __ for ___ minutes"
value={form.name}
onChange={(event) => {
handleFormChange("name", event.target.value);
}}
error={errors["name"] ? true : false}
helperText={errors["name"]}
/>
</Box>
</Stack>
<Stack direction="row">
<Box>
<Typography
component="h2"
variant="h2"
>
Add monitors
</Typography>
</Box>
<Box>
<Search
id={"monitors"}
multiple={true}
isAdorned={false}
options={monitors ? monitors : []}
filteredBy="name"
secondaryLabel={"type"}
inputValue={search}
value={form.monitors}
handleInputChange={handleSearch}
handleChange={handleSelectMonitors}
error={errors["monitors"]}
disabled={maintenanceWindowId !== undefined}
/>
</Box>
</Stack>
</ConfigBox>
</Box>
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
Monitors to apply maintenance window to
</Typography>
</Box>
<Stack gap={theme.spacing(15)}>
<Search
id={"monitors"}
label="Add monitors"
multiple={true}
isAdorned={false}
options={monitors ? monitors : []}
filteredBy="name"
secondaryLabel={"type"}
inputValue={search}
value={form.monitors}
handleInputChange={handleSearch}
handleChange={handleSelectMonitors}
error={errors["monitors"]}
disabled={maintenanceWindowId !== undefined}
/>
</Stack>
</ConfigBox>
<Box
ml="auto"
display="inline-block"
@@ -1,25 +0,0 @@
import { Stack, styled } from "@mui/material";
export const ConfigBox = styled(Stack)(({ theme }) => ({
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.light,
borderRadius: theme.spacing(2),
backgroundColor: theme.palette.background.main,
"& > *": { padding: theme.spacing(14) },
"& > :first-of-type, & > .MuiStack-root > div:first-of-type": {
flex: 0.6,
},
"& > div:last-of-type, & > .MuiStack-root > div:last-of-type": {
flex: 1,
},
"& > .MuiStack-root > div:first-of-type": { paddingRight: theme.spacing(14) },
"& > .MuiStack-root > div:last-of-type": {
paddingLeft: theme.spacing(14),
},
"& h2": { fontSize: 13.5, fontWeight: 500 },
"& h3, & p": {
color: theme.palette.text.tertiary,
},
"& h3": { fontWeight: 500 },
}));
+4 -3
View File
@@ -8,13 +8,14 @@ import { useSelector } from "react-redux";
import { networkService } from "../../main";
import Breadcrumbs from "../../Components/Breadcrumbs";
import { useNavigate } from "react-router-dom";
import { useIsAdmin } from "../../Hooks/useIsAdmin";
const Maintenance = ({ isAdmin }) => {
const Maintenance = () => {
const theme = useTheme();
const navigate = useNavigate();
const { authToken } = useSelector((state) => state.auth);
const { rowsPerPage } = useSelector((state) => state.ui.maintenance);
const isAdmin = useIsAdmin();
const [maintenanceWindows, setMaintenanceWindows] = useState([]);
const [maintenanceWindowCount, setMaintenanceWindowCount] = useState(0);
const [page, setPage] = useState(0);
@@ -99,7 +100,7 @@ const Maintenance = ({ isAdmin }) => {
"Stop sending alerts in maintenance windows",
]}
link="/maintenance/create"
isAdmin={true}
isAdmin={isAdmin}
/>
)}
</Box>
Whitespace-only changes.
@@ -1,96 +0,0 @@
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",
},
}));
export const IconBox = styled(Box)(({ theme }) => ({
height: 34,
minWidth: 34,
width: 34,
position: "relative",
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.dark,
borderRadius: 4,
backgroundColor: theme.palette.background.accent,
"& svg": {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 20,
height: 20,
"& path": {
stroke: theme.palette.text.tertiary,
},
},
}));
export const StatBox = styled(Box)(({ theme }) => ({
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
minWidth: 200,
width: 225,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.light,
borderRadius: 4,
backgroundColor: theme.palette.background.main,
background: `linear-gradient(340deg, ${theme.palette.background.accent} 20%, ${theme.palette.background.main} 45%)`,
"& h2": {
fontSize: 13,
fontWeight: 500,
color: theme.palette.text.secondary,
textTransform: "uppercase",
},
"& p": {
fontSize: 18,
color: theme.palette.text.primary,
marginTop: theme.spacing(2),
"& span": {
color: theme.palette.text.tertiary,
marginLeft: theme.spacing(2),
fontSize: 15,
},
},
}));
@@ -13,7 +13,7 @@ import {
import { monitorValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
import { ConfigBox } from "../../Monitors/styled";
import ConfigBox from "../../../Components/ConfigBox";
import TextInput from "../../../Components/Inputs/TextInput";
import Select from "../../../Components/Inputs/Select";
import Checkbox from "../../../Components/Inputs/Checkbox";
@@ -23,7 +23,7 @@ import PulseDot from "../../../Components/Animated/PulseDot";
import LoadingButton from "@mui/lab/LoadingButton";
import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded";
import SkeletonLayout from "./skeleton";
import useUtils from "../../Monitors/utils";
import useUtils from "../../Uptime/utils";
import "./index.css";
import Dialog from "../../../Components/Dialog";
@@ -135,6 +135,8 @@ const PageSpeedConfigure = () => {
if (pausePageSpeed.fulfilled.match(action)) {
const monitor = action.payload.data;
setMonitor(monitor);
const state = action?.payload?.data.isActive === false ? "paused" : "resumed";
createToast({ body: `Monitor ${state} successfully.` });
} else if (pausePageSpeed.rejected.match(action)) {
throw new Error(action.error.message);
}
@@ -1,42 +1,50 @@
import { useState } from "react";
import { Box, Button, ButtonGroup, Stack, Typography } from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton";
import { useSelector, useDispatch } from "react-redux";
import { monitorValidation } from "../../../Validation/validation";
// React, Redux, Router
import { useNavigate } from "react-router-dom";
import { useTheme } from "@emotion/react";
import { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
// Utility and Network
import { monitorValidation } from "../../../Validation/validation";
import {
createPageSpeed,
checkEndpointResolution,
} from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
import { ConfigBox } from "../../Monitors/styled";
import Radio from "../../../Components/Inputs/Radio";
import { parseDomainName } from "../../../Utils/monitorUtils";
// MUI
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton";
//Components
import Breadcrumbs from "../../../Components/Breadcrumbs";
import TextInput from "../../../Components/Inputs/TextInput";
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
import Select from "../../../Components/Inputs/Select";
import ConfigBox from "../../../Components/ConfigBox";
import { createToast } from "../../../Utils/toastUtils";
import Radio from "../../../Components/Inputs/Radio";
import Checkbox from "../../../Components/Inputs/Checkbox";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import { parseDomainName } from "../../../Utils/monitorUtils";
import "./index.css";
import Select from "../../../Components/Inputs/Select";
const MS_PER_MINUTE = 60000;
const CRUMBS = [
{ name: "pagespeed", path: "/pagespeed" },
{ name: "create", path: `/pagespeed/create` },
];
const SELECT_VALUES = [
{ _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 CreatePageSpeed = () => {
const MS_PER_MINUTE = 60000;
const { user, authToken } = useSelector((state) => state.auth);
const { isLoading } = useSelector((state) => state.pageSpeedMonitors);
const dispatch = useDispatch();
const navigate = useNavigate();
const theme = useTheme();
const idMap = {
"monitor-url": "url",
"monitor-name": "name",
"monitor-checks-http": "type",
"monitor-checks-ping": "type",
"notify-email-default": "notification-email",
};
// State
const [monitor, setMonitor] = useState({
url: "",
name: "",
@@ -44,71 +52,20 @@ const CreatePageSpeed = () => {
notifications: [],
interval: 3,
});
const [https, setHttps] = useState(true);
const [errors, setErrors] = useState({});
const { user, authToken } = useSelector((state) => state.auth);
const { isLoading } = useSelector((state) => state.pageSpeedMonitors);
const handleChange = (event, name) => {
const { value, id } = event.target;
if (!name) name = idMap[id];
if (name.includes("notification-")) {
name = name.replace("notification-", "");
let hasNotif = monitor.notifications.some(
(notification) => notification.type === name
);
setMonitor((prev) => {
const notifs = [...prev.notifications];
if (hasNotif) {
return {
...prev,
notifications: notifs.filter((notif) => notif.type !== name),
};
} else {
return {
...prev,
notifications: [
...notifs,
name === "email"
? { type: name, address: value }
: // TODO - phone number
{ type: name, phone: value },
],
};
}
});
} else {
setMonitor((prev) => ({
...prev,
[name]: value,
}));
const { error } = monitorValidation.validate(
{ [name]: value },
{ abortEarly: false }
);
setErrors((prev) => {
const updatedErrors = { ...prev };
if (error) updatedErrors[name] = error.details[0].message;
else delete updatedErrors[name];
return updatedErrors;
});
}
};
const onUrlBlur = (event) => {
const { value } = event.target;
if (monitor.name === "") {
setMonitor((prev) => ({
...prev,
name: parseDomainName(value),
}));
}
};
// Setup
const dispatch = useDispatch();
const navigate = useNavigate();
const theme = useTheme();
// Handlers
const handleCreateMonitor = async (event) => {
event.preventDefault();
//obj to submit
let form = {
url: `http${https ? "s" : ""}://` + monitor.url,
name: monitor.name === "" ? monitor.url : monitor.name,
@@ -126,46 +83,97 @@ const CreatePageSpeed = () => {
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
createToast({ body: "Error validation data." });
} else {
const checkEndpointAction = await dispatch(
checkEndpointResolution({ authToken, monitorURL: form.url })
);
if (checkEndpointAction.meta.requestStatus === "rejected") {
createToast({
body: "The endpoint you entered doesn't resolve. Check the URL again.",
});
setErrors({ url: "The entered URL is not reachable." });
return;
}
createToast({ body: "Please check the form for errors." });
return;
}
form = {
...form,
description: form.name,
teamId: user.teamId,
userId: user._id,
notifications: monitor.notifications,
};
const action = await dispatch(createPageSpeed({ authToken, monitor: form }));
if (action.meta.requestStatus === "fulfilled") {
createToast({ body: "Monitor created successfully!" });
navigate("/pagespeed");
} else {
createToast({ body: "Failed to create monitor." });
}
const checkEndpointAction = await dispatch(
checkEndpointResolution({ authToken, monitorURL: form.url })
);
if (checkEndpointAction.meta.requestStatus === "rejected") {
createToast({
body: "The endpoint you entered doesn't resolve. Check the URL again.",
});
setErrors({ url: "The entered URL is not reachable." });
return;
}
form = {
...form,
description: form.name,
teamId: user.teamId,
userId: user._id,
notifications: monitor.notifications,
};
const action = await dispatch(createPageSpeed({ authToken, monitor: form }));
if (action.meta.requestStatus === "fulfilled") {
createToast({ body: "Monitor created successfully!" });
navigate("/pagespeed");
} else {
createToast({ body: "Failed to create monitor." });
}
};
const handleChange = (event) => {
const { value, name } = event.target;
setMonitor({
...monitor,
[name]: value,
});
const { error } = monitorValidation.validate(
{ [name]: value },
{ abortEarly: false }
);
setErrors((prev) => ({
...prev,
...(error ? { [name]: error.details[0].message } : { [name]: undefined }),
}));
};
const handleNotifications = (event, type) => {
const { value } = event.target;
let notifications = [...monitor.notifications];
const notificationExists = notifications.some((notification) => {
if (notification.type === type && notification.address === value) {
return true;
}
return false;
});
if (notificationExists) {
notifications = notifications.filter((notification) => {
if (notification.type === type && notification.address === value) {
return false;
}
return true;
});
} else {
notifications.push({ type, address: value });
}
setMonitor((prev) => ({
...prev,
notifications,
}));
};
const handleBlur = (event) => {
const { name } = event.target;
if (name === "url") {
const { value } = event.target;
if (monitor.name !== "") {
return;
}
setMonitor((prev) => ({
...prev,
name: parseDomainName(value),
}));
}
};
//select values
const frequencies = [
{ _id: 3, name: "3 minutes" },
{ _id: 5, name: "5 minutes" },
{ _id: 10, name: "10 minutes" },
{ _id: 20, name: "20 minutes" },
{ _id: 60, name: "1 hour" },
{ _id: 1440, name: "1 day" },
{ _id: 10080, name: "1 week" },
];
return (
<Box
className="create-monitor"
@@ -175,12 +183,7 @@ const CreatePageSpeed = () => {
},
}}
>
<Breadcrumbs
list={[
{ name: "pagespeed", path: "/pagespeed" },
{ name: "create", path: `/pagespeed/create` },
]}
/>
<Breadcrumbs list={CRUMBS} />
<Stack
component="form"
className="create-monitor-form"
@@ -206,7 +209,7 @@ const CreatePageSpeed = () => {
fontWeight="inherit"
color={theme.palette.text.secondary}
>
pagespeed monitor
PageSpeed monitor
</Typography>
</Typography>
<ConfigBox>
@@ -219,19 +222,21 @@ const CreatePageSpeed = () => {
<Stack gap={theme.spacing(15)}>
<TextInput
type={"url"}
name="url"
id="monitor-url"
label="URL to monitor"
startAdornment={<HttpAdornment https={https} />}
placeholder="google.com"
value={monitor.url}
onChange={handleChange}
onBlur={onUrlBlur}
onBlur={handleBlur}
error={errors["url"] ? true : false}
helperText={errors["url"]}
/>
<TextInput
type="text"
id="monitor-name"
name="name"
label="Display name"
isOptional={true}
placeholder="Google"
@@ -253,12 +258,11 @@ const CreatePageSpeed = () => {
<Stack gap={theme.spacing(6)}>
<Radio
id="monitor-checks-http"
title="Website monitoring"
desc="Use HTTP(s) to monitor your website or API endpoint."
title="PageSpeed"
desc="Use the Lighthouse PageSpeed API to monitor your website"
size="small"
value="http"
checked={monitor.type === "pagespeed"}
onChange={(event) => handleChange(event)}
/>
<ButtonGroup sx={{ ml: "32px" }}>
<Button
@@ -269,8 +273,8 @@ const CreatePageSpeed = () => {
HTTPS
</Button>
<Button
variant="group"
filled={(!https).toString()}
variant="group" // Why does this work?
filled={(!https).toString()} // There's nothing in the docs about this either
onClick={() => setHttps(false)}
>
HTTP
@@ -301,14 +305,6 @@ const CreatePageSpeed = () => {
</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})`}
@@ -316,34 +312,8 @@ const CreatePageSpeed = () => {
(notification) => notification.type === "email"
)}
value={user?.email}
onChange={(event) => handleChange(event)}
onChange={(event) => handleNotifications(event, "email")}
/>
<Checkbox
id="notify-email"
label="Also notify via email to multiple addresses (coming soon)"
isChecked={false}
value=""
onChange={() => logger.warn("disabled")}
isDisabled={true}
/>
{monitor.notifications.some(
(notification) => notification.type === "emails"
) ? (
<Box mx={theme.spacing(16)}>
<TextInput
id="notify-email-list"
type="text"
placeholder="name@gmail.com"
value=""
onChange={() => logger.warn("disabled")}
/>
<Typography mt={theme.spacing(4)}>
You can separate multiple emails with a comma
</Typography>
</Box>
) : (
""
)}
</Stack>
</ConfigBox>
<ConfigBox>
@@ -353,10 +323,11 @@ const CreatePageSpeed = () => {
<Stack gap={theme.spacing(12)}>
<Select
id="monitor-interval"
name="interval"
label="Check frequency"
value={monitor.interval || 3}
onChange={(event) => handleChange(event, "interval")}
items={frequencies}
onChange={handleChange}
items={SELECT_VALUES}
/>
</Stack>
</ConfigBox>
@@ -368,7 +339,7 @@ const CreatePageSpeed = () => {
variant="contained"
color="primary"
onClick={handleCreateMonitor}
disabled={Object.keys(errors).length !== 0 && true}
disabled={!Object.values(errors).every((value) => value === undefined)}
loading={isLoading}
>
Create monitor
@@ -378,5 +349,4 @@ const CreatePageSpeed = () => {
</Box>
);
};
export default CreatePageSpeed;
@@ -101,21 +101,21 @@ PieValueLabel.propTypes = {
highlighted: PropTypes.bool.isRequired,
};
/**
* Weight constants for different performance metrics.
* @type {Object}
*/
const weights = {
fcp: 10,
si: 10,
lcp: 25,
tbt: 30,
cls: 25,
};
const PieChart = ({ audits }) => {
const theme = useTheme();
/**
* Weight constants for different performance metrics.
* @type {Object}
*/
const weights = {
fcp: 10,
si: 10,
lcp: 25,
tbt: 30,
cls: 25,
};
/**
* Retrieves color properties based on the performance value.
*
@@ -128,7 +128,7 @@ const PieChart = ({ audits }) => {
stroke: theme.palette.success.main,
strokeBg: theme.palette.success.light,
text: theme.palette.success.contrastText,
bg: theme.palette.success.dark,
bg: theme.palette.success.light,
};
else if (value >= 50 && value < 90)
return {
+26 -17
View File
@@ -5,7 +5,7 @@ import { useTheme } from "@emotion/react";
import { useNavigate, useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { formatDurationRounded, formatDurationSplit } from "../../../Utils/timeUtils";
import { ChartBox, IconBox, StatBox } from "./styled";
import { ChartBox } from "./styled";
import { logger } from "../../../Utils/Logger";
import { networkService } from "../../../main";
import SkeletonLayout from "./skeleton";
@@ -19,12 +19,16 @@ import PulseDot from "../../../Components/Animated/PulseDot";
import PagespeedDetailsAreaChart from "./Charts/AreaChart";
import Checkbox from "../../../Components/Inputs/Checkbox";
import PieChart from "./Charts/PieChart";
import useUtils from "../../Monitors/utils";
import useUtils from "../../Uptime/utils";
import "./index.css";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import StatBox from "../../../Components/StatBox";
import IconBox from "../../../Components/IconBox";
const PageSpeedDetails = ({ isAdmin }) => {
const PageSpeedDetails = () => {
const theme = useTheme();
const navigate = useNavigate();
const isAdmin = useIsAdmin();
const { statusColor, pagespeedStatusMsg, determineState } = useUtils();
const [monitor, setMonitor] = useState({});
const [audits, setAudits] = useState({});
@@ -185,20 +189,24 @@ const PageSpeedDetails = ({ isAdmin }) => {
direction="row"
gap={theme.spacing(8)}
>
<StatBox>
<Typography component="h2">checks since</Typography>
<Typography>
{splitDuration(monitor?.uptimeDuration)}
<Typography component="span">ago</Typography>
</Typography>
</StatBox>
<StatBox>
<Typography component="h2">last check</Typography>
<Typography>
{splitDuration(monitor?.lastChecked)}
<Typography component="span">ago</Typography>
</Typography>
</StatBox>
<StatBox
heading="checks since"
subHeading={
<>
{splitDuration(monitor?.uptimeDuration)}
<Typography component="span">ago</Typography>
</>
}
/>
<StatBox
heading="last check"
subHeading={
<>
{splitDuration(monitor?.lastChecked)}
<Typography component="span">ago</Typography>
</>
}
/>
</Stack>
<Box>
<Typography
@@ -286,6 +294,7 @@ const PageSpeedDetails = ({ isAdmin }) => {
</Box>
<ChartBox
flex={1}
/* TODO apply 1fr 1fr for columns, and auto 1fr for Rows */
sx={{ gridTemplateColumns: "50% 50%", gridTemplateRows: "15% 85%" }}
>
<Stack
+2 -53
View File
@@ -1,8 +1,8 @@
import { Box, Stack, styled } from "@mui/material";
import { Stack, styled } from "@mui/material";
export const ChartBox = styled(Stack)(({ theme }) => ({
display: "grid",
height: 300,
minHeight: 300,
minWidth: 250,
border: 1,
borderStyle: "solid",
@@ -40,54 +40,3 @@ export const ChartBox = styled(Stack)(({ theme }) => ({
transition: "stroke-width 400ms ease",
},
}));
export const IconBox = styled(Box)(({ theme }) => ({
height: 34,
minWidth: 34,
width: 34,
position: "relative",
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.dark,
borderRadius: 4,
backgroundColor: theme.palette.background.accent,
"& svg": {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 20,
height: 20,
"& path": {
stroke: theme.palette.text.tertiary,
},
},
}));
export const StatBox = styled(Box)(({ theme }) => ({
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
minWidth: 200,
width: 225,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.light,
borderRadius: 4,
backgroundColor: theme.palette.background.main,
background: `linear-gradient(340deg, ${theme.palette.background.accent} 20%, ${theme.palette.background.main} 45%)`,
"& h2": {
fontSize: 13,
fontWeight: 500,
color: theme.palette.text.secondary,
textTransform: "uppercase",
},
"& p": {
fontSize: 18,
color: theme.palette.text.primary,
marginTop: theme.spacing(2),
"& span": {
color: theme.palette.text.tertiary,
marginLeft: theme.spacing(2),
fontSize: 15,
},
},
}));
+2 -3
View File
@@ -4,13 +4,12 @@ import { StatusLabel } from "../../Components/Label";
import { Box, Grid, Stack, Typography } from "@mui/material";
import { useNavigate } from "react-router";
import { useTheme } from "@emotion/react";
import { IconBox } from "./Details/styled";
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts";
import { useSelector } from "react-redux";
import { formatDateWithTz, formatDurationSplit } from "../../Utils/timeUtils";
import useUtils from "../Monitors/utils";
import useUtils from "../Uptime/utils";
import { useState } from "react";
import IconBox from "../../Components/IconBox";
/**
* CustomToolTip displays a tooltip with formatted date and score information.
* @param {Object} props
+32 -8
View File
@@ -11,15 +11,17 @@ import Breadcrumbs from "../../Components/Breadcrumbs";
import SkeletonLayout from "./skeleton";
import Card from "./card";
import { networkService } from "../../main";
const PageSpeed = ({ isAdmin }) => {
import { Heading } from "../../Components/Heading";
import { useIsAdmin } from "../../Hooks/useIsAdmin";
const PageSpeed = () => {
const theme = useTheme();
const dispatch = useDispatch();
const navigate = useNavigate();
const isAdmin = useIsAdmin();
const { user, authToken } = useSelector((state) => state.auth);
const [isLoading, setIsLoading] = useState(true);
const [monitors, setMonitors] = useState([]);
const [monitorCount, setMonitorCount] = useState(0);
useEffect(() => {
dispatch(getPageSpeedByTeamId(authToken));
}, [authToken, dispatch]);
@@ -44,6 +46,7 @@ const PageSpeed = ({ isAdmin }) => {
});
if (res?.data?.data?.monitors) {
setMonitors(res.data.data.monitors);
setMonitorCount(res.data.data.monitorCount);
}
} catch (error) {
console.log(error);
@@ -53,7 +56,7 @@ const PageSpeed = ({ isAdmin }) => {
};
fetchMonitors();
}, []);
}, [authToken, user.teamId]);
// will show skeletons only on initial load
// since monitor state is being added to redux persist, there's no reason to display skeletons on every render
@@ -76,8 +79,8 @@ const PageSpeed = ({ isAdmin }) => {
{isActuallyLoading ? (
<SkeletonLayout />
) : monitors?.length !== 0 ? (
<Box>
<Box mb={theme.spacing(12)}>
<Stack gap={theme.spacing(8)}>
<Box>
<Breadcrumbs list={[{ name: `pagespeed`, path: "/pagespeed" }]} />
<Stack
direction="row"
@@ -90,13 +93,34 @@ const PageSpeed = ({ isAdmin }) => {
variant="contained"
color="primary"
onClick={() => navigate("/pagespeed/create")}
sx={{ whiteSpace: "nowrap" }}
sx={{ fontWeight: 500, whiteSpace: "nowrap" }}
>
Create new
</Button>
)}
</Stack>
</Box>
<Stack
direction="row"
sx={{
alignItems: "center",
gap: ".25rem",
flexWrap: "wrap",
}}
>
<Heading component="h2">PageSpeed monitors</Heading>
{/* TODO Correct the class current-monitors-counter, there are some unnecessary things there */}
<Box
component="span"
className="current-monitors-counter"
color={theme.palette.text.primary}
border={1}
borderColor={theme.palette.border.light}
backgroundColor={theme.palette.background.accent}
>
{monitorCount}
</Box>
</Stack>
<Grid
container
spacing={theme.spacing(12)}
@@ -108,7 +132,7 @@ const PageSpeed = ({ isAdmin }) => {
/>
))}
</Grid>
</Box>
</Stack>
) : (
<Fallback
title="pagespeed monitor"
+5 -27
View File
@@ -18,16 +18,16 @@ import LoadingButton from "@mui/lab/LoadingButton";
import { setTimezone, setMode } from "../../Features/UI/uiSlice";
import timezones from "../../Utils/timezones.json";
import { useState, useEffect } from "react";
import { ConfigBox } from "./styled";
import { networkService } from "../../main";
import { settingsValidation } from "../../Validation/validation";
import { useNavigate } from "react-router";
import Dialog from "../../Components/Dialog";
import { useIsAdmin } from "../../Hooks/useIsAdmin";
import ConfigBox from "../../Components/ConfigBox";
const SECONDS_PER_DAY = 86400;
const Settings = ({ isAdmin }) => {
const Settings = () => {
const theme = useTheme();
const isAdmin = useIsAdmin();
const { user, authToken } = useSelector((state) => state.auth);
const { checkTTL } = user;
const { isLoading } = useSelector((state) => state.uptimeMonitors);
@@ -43,7 +43,6 @@ const Settings = ({ isAdmin }) => {
const deleteStatsMonitorsInitState = { deleteMonitors: false, deleteStats: false };
const [isOpen, setIsOpen] = useState(deleteStatsMonitorsInitState);
const dispatch = useDispatch();
const navigate = useNavigate();
//Fetching latest release version from github
useEffect(() => {
@@ -322,28 +321,7 @@ const Settings = ({ isAdmin }) => {
/>
</ConfigBox>
)}
{isAdmin && (
<ConfigBox>
<Box>
<Typography component="h1">Advanced settings</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Click here to modify advanced settings
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Box>
<Button
variant="contained"
onClick={() => {
navigate("/advanced-settings");
}}
>
Advanced Settings
</Button>
</Box>
</Stack>
</ConfigBox>
)}
<ConfigBox>
<Box>
<Typography component="h1">About</Typography>
-28
View File
@@ -1,28 +0,0 @@
import { Stack, styled } from "@mui/material";
export const ConfigBox = styled(Stack)(({ theme }) => ({
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
gap: theme.spacing(20),
paddingTop: theme.spacing(12),
paddingInline: theme.spacing(15),
paddingBottom: theme.spacing(25),
backgroundColor: theme.palette.background.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.light,
borderRadius: theme.spacing(2),
"& > div:first-of-type": {
flex: 0.7,
},
"& > div:last-of-type": {
flex: 1,
},
"& h1, & h2": {
color: theme.palette.text.secondary,
},
"& p": {
color: theme.palette.text.tertiary,
},
}));
@@ -6,7 +6,7 @@ import { Box, Stack, Tooltip, Typography } from "@mui/material";
import { monitorValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
import { ConfigBox } from "../styled";
import ConfigBox from "../../../Components/ConfigBox";
import {
updateUptimeMonitor,
pauseUptimeMonitor,
@@ -141,6 +141,8 @@ const Configure = () => {
if (pauseUptimeMonitor.fulfilled.match(action)) {
const monitor = action.payload.data;
setMonitor(monitor);
const state = action?.payload?.data.isActive === false ? "paused" : "resumed";
createToast({ body: `Monitor ${state} successfully.` });
} else if (pauseUptimeMonitor.rejected.match(action)) {
throw new Error(action.error.message);
}
@@ -166,7 +168,7 @@ const Configure = () => {
event.preventDefault();
const action = await dispatch(deleteUptimeMonitor({ authToken, monitor }));
if (action.meta.requestStatus === "fulfilled") {
navigate("/monitors");
navigate("/uptime");
} else {
createToast({ body: "Failed to delete monitor." });
}
@@ -207,9 +209,9 @@ const Configure = () => {
<>
<Breadcrumbs
list={[
{ name: "monitors", path: "/monitors" },
{ name: "details", path: `/monitors/${monitorId}` },
{ name: "configure", path: `/monitors/configure/${monitorId}` },
{ name: "uptime", path: "/uptime" },
{ name: "details", path: `/uptime/${monitorId}` },
{ name: "configure", path: `/uptime/configure/${monitorId}` },
]}
/>
<Stack
@@ -282,7 +284,7 @@ const Configure = () => {
},
}}
>
Editting...
Editing...
</Typography>
</Stack>
</Box>
@@ -17,14 +17,13 @@ import LoadingButton from "@mui/lab/LoadingButton";
//Components
import Breadcrumbs from "../../../Components/Breadcrumbs";
import { ConfigBox } from "../styled";
import TextInput from "../../../Components/Inputs/TextInput";
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
import { createToast } from "../../../Utils/toastUtils";
import Radio from "../../../Components/Inputs/Radio";
import Checkbox from "../../../Components/Inputs/Checkbox";
import Select from "../../../Components/Inputs/Select";
import ConfigBox from "../../../Components/ConfigBox";
const CreateMonitor = () => {
const MS_PER_MINUTE = 60000;
const SELECT_VALUES = [
@@ -60,8 +59,8 @@ const CreateMonitor = () => {
const theme = useTheme();
const { monitorId } = useParams();
const crumbs = [
{ name: "monitors", path: "/monitors" },
{ name: "create", path: `/monitors/create` },
{ name: "uptime", path: "/uptime" },
{ name: "create", path: `/uptime/create` },
];
// State
@@ -125,7 +124,7 @@ const CreateMonitor = () => {
const action = await dispatch(createUptimeMonitor({ authToken, monitor: form }));
if (action.meta.requestStatus === "fulfilled") {
createToast({ body: "Monitor created successfully!" });
navigate("/monitors");
navigate("/uptime");
} else {
createToast({ body: "Failed to create monitor." });
}
@@ -345,8 +344,6 @@ const CreateMonitor = () => {
</Typography>
</Box>
<Stack gap={theme.spacing(6)}>
<Typography component="p">When there is a new incident,</Typography>
<Checkbox
id="notify-email-default"
label={`Notify via email (to ${user.email})`}
@@ -262,7 +262,7 @@ const ResponseGaugeChart = ({ data }) => {
}
: {
category: "Poor",
main: theme.palette.error.contrastText,
main: theme.palette.error.light,
bg: theme.palette.error.dark,
};
@@ -26,20 +26,23 @@ import HistoryIcon from "../../../assets/icons/history-icon.svg?react";
import PaginationTable from "./PaginationTable";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import PulseDot from "../../../Components/Animated/PulseDot";
import { StatBox, ChartBox, IconBox } from "./styled";
import { ChartBox } from "./styled";
import { DownBarChart, ResponseGaugeChart, UpBarChart } from "./Charts";
import SkeletonLayout from "./skeleton";
import "./index.css";
import useUtils from "../utils";
import { formatDateWithTz } from "../../../Utils/timeUtils";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import IconBox from "../../../Components/IconBox";
import StatBox from "../../../Components/StatBox";
/**
* Details page component displaying monitor details and related information.
* @component
*/
const DetailsPage = ({ isAdmin }) => {
const DetailsPage = () => {
const theme = useTheme();
const { statusColor, statusStyles, statusMsg, determineState } = useUtils();
const isAdmin = useIsAdmin();
const [monitor, setMonitor] = useState({});
const { monitorId } = useParams();
const { authToken } = useSelector((state) => state.auth);
@@ -100,7 +103,7 @@ const DetailsPage = ({ isAdmin }) => {
}
};
fetchCertificate();
}, [authToken, monitorId, monitor]);
}, [authToken, monitorId, monitor, uiTimezone, dateFormat]);
const splitDuration = (duration) => {
const { time, format } = formatDurationSplit(duration);
@@ -117,18 +120,17 @@ const DetailsPage = ({ isAdmin }) => {
const [hoveredUptimeData, setHoveredUptimeData] = useState(null);
const [hoveredIncidentsData, setHoveredIncidentsData] = useState(null);
const BREADCRUMBS = [
{ name: "uptime", path: "/uptime" },
{ name: "details", path: `/uptime/${monitorId}` },
];
return (
<Box className="monitor-details">
{loading ? (
<SkeletonLayout />
) : (
<>
<Breadcrumbs
list={[
{ name: "monitors", path: "/monitors" },
{ name: "details", path: `/monitors/${monitorId}` },
]}
/>
<Breadcrumbs list={BREADCRUMBS} />
<Stack
gap={theme.spacing(10)}
mt={theme.spacing(10)}
@@ -262,7 +264,7 @@ const DetailsPage = ({ isAdmin }) => {
<Button
variant="contained"
color="secondary"
onClick={() => navigate(`/monitors/configure/${monitorId}`)}
onClick={() => navigate(`/uptime/configure/${monitorId}`)}
sx={{
px: theme.spacing(5),
"& svg": {
@@ -282,24 +284,24 @@ const DetailsPage = ({ isAdmin }) => {
direction="row"
gap={theme.spacing(8)}
>
<StatBox sx={statusStyles[determineState(monitor)]}>
<Typography component="h2">active for</Typography>
<Typography>{splitDuration(monitor?.uptimeDuration)}</Typography>
</StatBox>
<StatBox>
<Typography component="h2">last check</Typography>
<Typography>
{splitDuration(monitor?.lastChecked)}
<Typography component="span">ago</Typography>
</Typography>
</StatBox>
<StatBox>
<Typography component="h2">last response time</Typography>
<Typography>
{monitor?.latestResponseTime}
<Typography component="span">ms</Typography>
</Typography>
</StatBox>
<StatBox
sx={statusStyles[determineState(monitor)]}
heading={"active for"}
subHeading={splitDuration(monitor?.uptimeDuration)}
/>
<StatBox
heading="last check"
subHeading={splitDuration(monitor?.lastChecked)}
/>
<StatBox
heading="last response time"
subHeading={
<>
{monitor?.latestResponseTime}
<Typography component="span">{"ms"}</Typography>
</>
}
/>
</Stack>
<Box>
<Stack
@@ -0,0 +1,45 @@
import { 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",
},
}));
@@ -1,13 +1,13 @@
import { useTheme } from "@emotion/react";
import { Box, Stack } from "@mui/material";
import Search from "../../../../Components/Inputs/Search";
import MemoizedMonitorTable from "../MonitorTable";
import MemoizedMonitorTable from "../UptimeTable";
import { useState } from "react";
import useDebounce from "../../../../Utils/debounce";
import PropTypes from "prop-types";
import { Heading } from "../../../../Components/Heading";
const CurrentMonitoring = ({ totalMonitors, monitors, isAdmin }) => {
const CurrentMonitoring = ({ totalMonitors, monitors, isAdmin, handlePause }) => {
const theme = useTheme();
const [search, setSearch] = useState("");
const [isSearching, setIsSearching] = useState(false);
@@ -26,7 +26,7 @@ const CurrentMonitoring = ({ totalMonitors, monitors, isAdmin }) => {
alignItems="center"
mb={theme.spacing(8)}
>
<Heading component="h2">Actively monitoring</Heading>
<Heading component="h2">Uptime monitors</Heading>
<Box
className="current-monitors-counter"
@@ -55,12 +55,14 @@ const CurrentMonitoring = ({ totalMonitors, monitors, isAdmin }) => {
filter={debouncedFilter}
setIsSearching={setIsSearching}
isSearching={isSearching}
handlePause={handlePause}
/>
</Box>
);
};
CurrentMonitoring.propTypes = {
handlePause: PropTypes.func,
totalMonitors: PropTypes.number,
monitors: PropTypes.array,
isAdmin: PropTypes.bool,
@@ -32,7 +32,7 @@ import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
import { Pagination } from "../../../Infrastructure/components/TablePagination";
const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching, handlePause }) => {
const theme = useTheme();
const navigate = useNavigate();
const dispatch = useDispatch();
@@ -48,7 +48,7 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
const [sort, setSort] = useState({});
const prevFilter = useRef(filter);
const handleActionMenuDelete = () => {
const handleRowUpdate = () => {
setUpdateTrigger((prev) => !prev);
};
@@ -144,7 +144,7 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
setMonitors(res?.data?.data?.monitors ?? []);
setMonitorCount(res?.data?.data?.monitorCount ?? 0);
};
/* TODO Apply component basic table? */
return (
<Box position="relative">
{isSearching && (
@@ -264,11 +264,13 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
sx={{
cursor: "pointer",
"&:hover": {
backgroundColor: theme.palette.background.accent,
filter: "brightness(.75)",
opacity: 0.75,
transition: "filter 0.3s ease, opacity 0.3s ease",
},
}}
onClick={() => {
navigate(`/monitors/${monitor._id}`);
navigate(`/uptime/${monitor._id}`);
}}
>
<TableCell>
@@ -297,7 +299,8 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
<ActionsMenu
monitor={monitor}
isAdmin={isAdmin}
updateCallback={handleActionMenuDelete}
updateRowCallback={handleRowUpdate}
pauseCallback={handlePause}
/>
</TableCell>
</TableRow>
@@ -325,6 +328,8 @@ MonitorTable.propTypes = {
filter: PropTypes.string,
setIsSearching: PropTypes.func,
isSearching: PropTypes.bool,
setMonitorUpdateTrigger: PropTypes.func,
handlePause: PropTypes.func,
};
const MemoizedMonitorTable = memo(MonitorTable);
@@ -14,7 +14,7 @@ import Settings from "../../../assets/icons/settings-bold.svg?react";
import PropTypes from "prop-types";
import Dialog from "../../../Components/Dialog";
const ActionsMenu = ({ monitor, isAdmin, updateCallback }) => {
const ActionsMenu = ({ monitor, isAdmin, updateRowCallback, pauseCallback }) => {
const [anchorEl, setAnchorEl] = useState(null);
const [actions, setActions] = useState({});
const [isOpen, setIsOpen] = useState(false);
@@ -47,9 +47,9 @@ const ActionsMenu = ({ monitor, isAdmin, updateCallback }) => {
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.` });
pauseCallback();
} else {
throw new Error(action?.error?.message ?? "Failed to pause monitor.");
}
@@ -131,7 +131,7 @@ const ActionsMenu = ({ monitor, isAdmin, updateCallback }) => {
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/monitors/${actions.id}`);
navigate(`/uptime/${actions.id}`);
}}
>
Details
@@ -150,7 +150,7 @@ const ActionsMenu = ({ monitor, isAdmin, updateCallback }) => {
onClick={(e) => {
e.stopPropagation();
navigate(`/monitors/configure/${actions.id}`);
navigate(`/uptime/configure/${actions.id}`);
}}
>
Configure
@@ -160,7 +160,7 @@ const ActionsMenu = ({ monitor, isAdmin, updateCallback }) => {
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/monitors/create/${actions.id}`);
navigate(`/uptime/create/${actions.id}`);
}}
>
Clone
@@ -219,7 +219,8 @@ ActionsMenu.propTypes = {
isActive: PropTypes.bool,
}).isRequired,
isAdmin: PropTypes.bool,
updateCallback: PropTypes.func,
updateRowCallback: PropTypes.func,
pauseCallback: PropTypes.func,
};
export default ActionsMenu;
@@ -41,7 +41,7 @@ const Fallback = ({ isAdmin }) => {
variant="contained"
color="primary"
onClick={() => {
navigate("/monitors/create");
navigate("/uptime/create");
}}
sx={{ mt: theme.spacing(12) }}
>
@@ -1,5 +1,5 @@
import "./index.css";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getUptimeMonitorsByTeamId } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import { useNavigate } from "react-router-dom";
@@ -12,23 +12,32 @@ import StatusBox from "./StatusBox";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import Greeting from "../../../Utils/greeting";
import { CurrentMonitoring } from "./CurrentMonitoring";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
const Monitors = ({ isAdmin }) => {
const BREADCRUMBS = [{ name: `Uptime`, path: "/uptime" }];
const UptimeMonitors = () => {
const theme = useTheme();
const navigate = useNavigate();
const monitorState = useSelector((state) => state.uptimeMonitors);
const isAdmin = useIsAdmin();
const uptimeMonitorsState = useSelector((state) => state.uptimeMonitors);
const authState = useSelector((state) => state.auth);
const dispatch = useDispatch({});
const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false);
const handlePause = () => {
setMonitorUpdateTrigger((prev) => !prev);
};
useEffect(() => {
dispatch(getUptimeMonitorsByTeamId(authState.authToken));
}, [authState.authToken, dispatch]);
}, [authState.authToken, dispatch, monitorUpdateTrigger]);
//TODO bring fetching to this component, like on pageSpeed
const loading = monitorState?.isLoading;
const loading = uptimeMonitorsState?.isLoading;
const totalMonitors = monitorState?.monitorsSummary?.monitorCounts?.total;
const totalMonitors = uptimeMonitorsState?.monitorsSummary?.monitorCounts?.total;
const hasMonitors = totalMonitors > 0;
const noMonitors = !hasMonitors;
@@ -40,34 +49,34 @@ const Monitors = ({ isAdmin }) => {
gap={theme.spacing(8)}
>
<Box>
<Breadcrumbs list={[{ name: `monitors`, path: "/monitors" }]} />
<Breadcrumbs list={BREADCRUMBS} />
<Stack
direction="row"
justifyContent="space-between"
justifyContent="end"
alignItems="center"
mt={theme.spacing(5)}
gap={theme.spacing(6)}
>
<Greeting type="uptime" />
{canAddMonitor && (
<Button
variant="contained"
color="primary"
onClick={() => {
navigate("/monitors/create");
navigate("/uptime/create");
}}
sx={{ fontWeight: 500 }}
sx={{ fontWeight: 500, whiteSpace: "nowrap" }}
>
Create monitor
Create new
</Button>
)}
</Stack>
<Greeting type="uptime" />
</Box>
{noMonitors && <Fallback isAdmin={isAdmin} />}
{loading ? (
<SkeletonLayout />
) : (
<>
{noMonitors && <Fallback isAdmin={isAdmin} />}
{hasMonitors && (
<>
<Stack
@@ -77,21 +86,22 @@ const Monitors = ({ isAdmin }) => {
>
<StatusBox
title="up"
value={monitorState?.monitorsSummary?.monitorCounts?.up ?? 0}
value={uptimeMonitorsState?.monitorsSummary?.monitorCounts?.up ?? 0}
/>
<StatusBox
title="down"
value={monitorState?.monitorsSummary?.monitorCounts?.down ?? 0}
value={uptimeMonitorsState?.monitorsSummary?.monitorCounts?.down ?? 0}
/>
<StatusBox
title="paused"
value={monitorState?.monitorsSummary?.monitorCounts?.paused ?? 0}
value={uptimeMonitorsState?.monitorsSummary?.monitorCounts?.paused ?? 0}
/>
</Stack>
<CurrentMonitoring
isAdmin={isAdmin}
monitors={monitorState.monitorsSummary.monitors}
monitors={uptimeMonitorsState.monitorsSummary.monitors}
totalMonitors={totalMonitors}
handlePause={handlePause}
/>
</>
)}
@@ -101,7 +111,7 @@ const Monitors = ({ isAdmin }) => {
);
};
Monitors.propTypes = {
UptimeMonitors.propTypes = {
isAdmin: PropTypes.bool,
};
export default Monitors;
export default UptimeMonitors;
File renamed without changes.
+171
View File
@@ -0,0 +1,171 @@
import { Navigate, Route, Routes as LibRoutes } from "react-router";
import HomeLayout from "../Components/Layouts/HomeLayout";
import { Infrastructure } from "../Pages/Infrastructure";
import InfrastructureDetails from "../Pages/Infrastructure/Details";
import NotFound from "../Pages/NotFound";
import Login from "../Pages/Auth/Login/Login";
import Register from "../Pages/Auth/Register/Register";
import Account from "../Pages/Account";
import Monitors from "../Pages/Uptime/Home";
import CreateMonitor from "../Pages/Uptime/CreateUptime";
import CreateInfrastructureMonitor from "../Pages/Infrastructure/CreateMonitor";
import Incidents from "../Pages/Incidents";
import Status from "../Pages/Status";
import Integrations from "../Pages/Integrations";
import Settings from "../Pages/Settings";
import ForgotPassword from "../Pages/Auth/ForgotPassword";
import CheckEmail from "../Pages/Auth/CheckEmail";
import SetNewPassword from "../Pages/Auth/SetNewPassword";
import NewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed";
import ProtectedRoute from "../Components/ProtectedRoute";
import Details from "../Pages/Uptime/Details";
import Maintenance from "../Pages/Maintenance";
import Configure from "../Pages/Uptime/Configure";
import PageSpeed from "../Pages/PageSpeed";
import CreatePageSpeed from "../Pages/PageSpeed/CreatePageSpeed";
import CreateNewMaintenanceWindow from "../Pages/Maintenance/CreateMaintenance";
import PageSpeedDetails from "../Pages/PageSpeed/Details";
import PageSpeedConfigure from "../Pages/PageSpeed/Configure";
import withAdminCheck from "../Components/HOC/withAdminCheck";
const Routes = () => {
const AdminCheckedRegister = withAdminCheck(Register);
return (
<LibRoutes>
<Route
path="/"
element={
<ProtectedRoute>
<HomeLayout />
</ProtectedRoute>
}
>
<Route
path="/"
element={<Navigate to="/uptime" />}
/>
<Route
path="/uptime"
element={<Monitors />}
/>
<Route
path="/uptime/create/:monitorId?"
element={<CreateMonitor />}
/>
<Route
path="/uptime/:monitorId/"
element={<Details />}
/>
<Route
path="/uptime/configure/:monitorId/"
element={<Configure />}
/>
<Route
path="pagespeed"
element={<PageSpeed />}
/>
<Route
path="pagespeed/create"
element={<CreatePageSpeed />}
/>
<Route
path="pagespeed/:monitorId"
element={<PageSpeedDetails />}
/>
<Route
path="pagespeed/configure/:monitorId"
element={<PageSpeedConfigure />}
/>
<Route
path="infrastructure"
element={<Infrastructure />}
/>
<Route
path="infrastructure/create"
element={<CreateInfrastructureMonitor />}
/>
<Route
path="infrastructure/:monitorId"
element={<InfrastructureDetails />}
/>
<Route
path="incidents/:monitorId?"
element={<Incidents />}
/>
<Route
path="status"
element={<Status />}
/>
<Route
path="integrations"
element={<Integrations />}
/>
<Route
path="maintenance"
element={<Maintenance />}
/>
<Route
path="/maintenance/create/:maintenanceWindowId?"
element={<CreateNewMaintenanceWindow />}
/>
<Route
path="settings"
element={<Settings />}
/>
<Route
path="account/profile"
element={<Account open={"profile"} />}
/>
<Route
path="account/password"
element={<Account open={"password"} />}
/>
<Route
path="account/team"
element={<Account open={"team"} />}
/>
</Route>
<Route
path="/login"
element={<Login />}
/>
<Route
path="/register"
element={<AdminCheckedRegister />}
/>
<Route
exact
path="/register/:token"
element={<Register />}
/>
<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 />}
/>
<Route
path="*"
element={<NotFound />}
/>
</LibRoutes>
);
};
export { Routes };
+9 -2
View File
@@ -46,6 +46,7 @@ const paletteColors = {
green50: "#D4F4E1",
green150: "#45BB7A",
green400: "#079455",
green500: "#07B467",
green800: "#1C4428",
green900: "#12261E",
red50: "#F9ECED",
@@ -96,10 +97,10 @@ const semanticColors = {
},
contrastText: {
light: paletteColors.green50,
dark: paletteColors.green900,
dark: paletteColors.green50,
},
light: {
light: paletteColors.green50,
light: paletteColors.green500,
dark: paletteColors.green800,
},
dark: {
@@ -107,6 +108,12 @@ const semanticColors = {
dark: paletteColors.green900,
},
},
neutral: {
contrastText: {
light: paletteColors.blueGray900,
dark: paletteColors.gray100,
},
},
error: {
main: {
light: paletteColors.red300,
+4
View File
@@ -8,6 +8,7 @@ const {
success,
error,
warning,
neutral,
gradient: {
color1: { dark: color1 },
color2: { dark: color2 },
@@ -51,6 +52,9 @@ const palette = {
contrastText: warning.contrastText.dark,
dark: warning.dark.dark,
},
neutral: {
contrastText: neutral.contrastText.dark,
},
/* From this part on, try to create semantic structure, not feature based structure */
percentage: {
uptimePoor: error.main.dark,
+7 -1
View File
@@ -47,7 +47,8 @@ const baseTheme = (palette) => ({
{
props: (props) => props.variant === "group",
style: {
color: theme.palette.secondary.contrastText,
/* color: theme.palette.secondary.contrastText, */
color: theme.palette.neutral.contrastText,
backgroundColor: theme.palette.background.main,
border: 1,
borderStyle: "solid",
@@ -237,6 +238,11 @@ const baseTheme = (palette) => ({
WebkitTextFillColor: "unset",
},
},
"& .MuiInputBase-input:-webkit-autofill": {
transition: "background-color 5000s ease-in-out 0s",
WebkitBoxShadow: `0 0 0px 1000px ${theme.palette.background.main} inset`,
WebkitTextFillColor: theme.palette.text.primary,
},
"& .MuiInputBase-input.MuiOutlinedInput-input": {
padding: "0 var(--env-var-spacing-1-minus) !important",
},
+4
View File
@@ -13,6 +13,7 @@ const {
success,
error,
warning,
neutral,
gradient: {
color1: { light: color1 },
color2: { light: color2 },
@@ -57,6 +58,9 @@ const palette = {
contrastText: warning.contrastText.light,
dark: warning.dark.light,
},
neutral: {
contrastText: neutral.contrastText.light,
},
/* From this part on, try to create semantic structure, not feature based structure */
percentage: {
uptimePoor: error.main.light,
+36 -8
View File
@@ -91,10 +91,24 @@ const credentials = joi.object({
});
const monitorValidation = joi.object({
url: joi.string().uri({ allowRelative: true }).trim().messages({
"string.empty": "This field is required.",
"string.uri": "The URL you provided is not valid.",
}),
url: joi
.string()
.trim()
.custom((value, helpers) => {
const urlRegex =
/^(https?:\/\/)?(([0-9]{1,3}\.){3}[0-9]{1,3}|[\da-z\.-]+)(\.[a-z\.]{2,6})?(:(\d+))?([\/\w \.-]*)*\/?$/i;
if (!urlRegex.test(value)) {
return helpers.error("string.invalidUrl");
}
return value;
})
.messages({
"string.empty": "This field is required.",
"string.uri": "The URL you provided is not valid.",
"string.invalidUrl": "Please enter a valid URL with optional port",
}),
name: joi.string().trim().max(50).allow("").messages({
"string.max": "This field should not exceed the 50 characters limit.",
}),
@@ -176,10 +190,24 @@ const advancedSettingsValidation = joi.object({
});
const infrastructureMonitorValidation = joi.object({
url: joi.string().uri({ allowRelative: true }).trim().messages({
"string.empty": "This field is required.",
"string.uri": "The URL you provided is not valid.",
}),
url: joi
.string()
.trim()
.custom((value, helpers) => {
const urlRegex =
/^(https?:\/\/)?(([0-9]{1,3}\.){3}[0-9]{1,3}|[\da-z\.-]+)(\.[a-z\.]{2,6})?(:(\d+))?([\/\w \.-]*)*\/?$/i;
if (!urlRegex.test(value)) {
return helpers.error("string.invalidUrl");
}
return value;
})
.messages({
"string.empty": "This field is required.",
"string.uri": "The URL you provided is not valid.",
"string.invalidUrl": "Please enter a valid URL with optional port",
}),
name: joi.string().trim().max(50).allow("").messages({
"string.max": "This field should not exceed the 50 characters limit.",
}),
+4
View File
@@ -1,6 +1,7 @@
services:
client:
image: uptime_client:latest
restart: always
ports:
- "5173:5173"
@@ -8,6 +9,7 @@ services:
- server
server:
image: uptime_server:latest
restart: always
ports:
- "5000:5000"
env_file:
@@ -17,12 +19,14 @@ services:
- mongodb
redis:
image: uptime_redis:latest
restart: always
ports:
- "6379:6379"
volumes:
- ./redis/data:/data
mongodb:
image: uptime_database_mongo:latest
restart: always
command: ["mongod", "--quiet"]
ports:
- "27017:27017"
+4
View File
@@ -1,6 +1,7 @@
services:
client:
image: bluewaveuptime/uptime_client:latest
restart: always
environment:
UPTIME_APP_API_BASE_URL: "http://localhost:5000/api/v1"
ports:
@@ -10,6 +11,7 @@ services:
- server
server:
image: bluewaveuptime/uptime_server:latest
restart: always
ports:
- "5000:5000"
depends_on:
@@ -22,12 +24,14 @@ services:
# - /var/run/docker.sock:/var/run/docker.sock:ro
redis:
image: bluewaveuptime/uptime_redis:latest
restart: always
ports:
- "6379:6379"
volumes:
- ./redis/data:/data
mongodb:
image: bluewaveuptime/uptime_database_mongo:latest
restart: always
volumes:
- ./mongo/data:/data/db
command: ["mongod", "--quiet"]
+1 -1
View File
@@ -2,7 +2,7 @@ server {
listen 80;
listen [::]:80;
server_name uptime-demo.bluewavelabs.ca;
server_name checkmate-demo.bluewavelabs.ca;
server_tokens off;
location /.well-known/acme-challenge/ {
+6 -1
View File
@@ -1,8 +1,9 @@
services:
client:
image: uptime_client:latest
restart: always
environment:
UPTIME_APP_API_BASE_URL: "https://uptime-demo.bluewavelabs.ca/api/v1"
UPTIME_APP_API_BASE_URL: "https://checkmate-demo.bluewavelabs.ca/api/v1"
ports:
- "80:80"
- "443:443"
@@ -15,11 +16,13 @@ services:
certbot:
image: certbot/certbot:latest
restart: always
volumes:
- ./certbot/www/:/var/www/certbot/:rw
- ./certbot/conf/:/etc/letsencrypt/:rw
server:
image: uptime_server:latest
restart: always
ports:
- "5000:5000"
env_file:
@@ -29,12 +32,14 @@ services:
- mongodb
redis:
image: uptime_redis:latest
restart: always
ports:
- "6379:6379"
volumes:
- ./redis/data:/data
mongodb:
image: uptime_database_mongo:latest
restart: always
command: ["mongod", "--quiet", "--auth"]
ports:
- "27017:27017"
+2
View File
@@ -1,5 +1,7 @@
FROM node:20-alpine
ENV NODE_OPTIONS="--max-old-space-size=2048"
WORKDIR /app
COPY ./Server/package*.json ./
+5 -1
View File
@@ -1,8 +1,9 @@
services:
client:
image: uptime_client:latest
restart: always
environment:
UPTIME_APP_API_BASE_URL: "https://uptime-demo.bluewavelabs.ca/api/v1"
UPTIME_APP_API_BASE_URL: "https://checkmate-test.bluewavelabs.ca/api/v1"
ports:
- "80:80"
- "443:443"
@@ -15,11 +16,13 @@ services:
certbot:
image: certbot/certbot:latest
restart: always
volumes:
- ./certbot/www/:/var/www/certbot/:rw
- ./certbot/conf/:/etc/letsencrypt/:rw
server:
image: uptime_server:latest
restart: always
ports:
- "5000:5000"
env_file:
@@ -29,6 +32,7 @@ services:
- mongodb
redis:
image: uptime_redis:latest
restart: always
ports:
- "6379:6379"
volumes:
+1 -1
View File
@@ -2,7 +2,7 @@ services:
client:
image: uptime_client:latest
environment:
UPTIME_APP_API_BASE_URL: "https://uptime-demo.bluewavelabs.ca/api/v1"
UPTIME_APP_API_BASE_URL: "https://checkmate-demo.bluewavelabs.ca/api/v1"
ports:
- "80:80"
- "443:443"
+4 -4
View File
@@ -2,7 +2,7 @@ server {
listen 80;
listen [::]:80;
server_name uptime-demo.bluewavelabs.ca;
server_name checkmate-demo.bluewavelabs.ca;
server_tokens off;
location /.well-known/acme-challenge/ {
@@ -38,10 +38,10 @@ server {
listen 443 default_server ssl http2;
listen [::]:443 ssl http2;
server_name uptime-demo.bluewavelabs.ca;
server_name checkmate-demo.bluewavelabs.ca;
ssl_certificate /etc/nginx/ssl/live/uptime-demo.bluewavelabs.ca/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/live/uptime-demo.bluewavelabs.ca/privkey.pem;
ssl_certificate /etc/nginx/ssl/live/checkmate-demo.bluewavelabs.ca/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/live/checkmate-demo.bluewavelabs.ca/privkey.pem;
location / {
root /usr/share/nginx/html;
+59 -31
View File
@@ -1,35 +1,49 @@
![](https://img.shields.io/github/license/bluewave-labs/bluewave-uptime)
![](https://img.shields.io/github/repo-size/bluewave-labs/bluewave-uptime)
![](https://img.shields.io/github/commit-activity/w/bluewave-labs/bluewave-uptime)
![](https://img.shields.io/github/last-commit/bluewave-labs/bluewave-uptime)
![](https://img.shields.io/github/languages/top/bluewave-labs/bluewave-uptime)
![](https://img.shields.io/github/issues-pr/bluewave-labs/bluewave-uptime)
![](https://img.shields.io/github/issues/bluewave-labs/bluewave-uptime)
**Need support or have a suggestion? Check our [Discord channel](https://discord.gg/NAb6H3UTjK) or [Discussions](https://github.com/bluewave-labs/checkmate/discussions) forum.**
**Checkmate is on [GitHub trending](https://github.com/trending) and #1 trending for JavaScript apps on Fri Dec 13th & 15th**
**If you would like to support us, please consider giving it a ⭐, and think about contributing or providing feedback.**
![Frame 34](https://github.com/user-attachments/assets/4bf57845-3f47-4759-835b-285a5486191d)
![](https://img.shields.io/github/license/bluewave-labs/checkmate)
![](https://img.shields.io/github/repo-size/bluewave-labs/checkmate)
![](https://img.shields.io/github/commit-activity/m/bluewave-labs/checkmate)
![](https://img.shields.io/github/last-commit/bluewave-labs/checkmate)
![](https://img.shields.io/github/languages/top/bluewave-labs/checkmate)
![](https://img.shields.io/github/issues/bluewave-labs/checkmate)
![](https://img.shields.io/github/issues-pr/bluewave-labs/checkmate)
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
<p align="center"><strong>An open source server monitoring application</strong></p>
<p align="center"><strong>An open source server and infrastructure monitoring application</strong></p>
![Dashboard-dark](https://github.com/user-attachments/assets/db875138-164f-453c-a75e-889f88747578)
(yes, we have a light theme as well, but this looks better on readme.md)
Checkmate is an open source uptime manager, server & Docker monitoring tool used to track the operational status and performance of servers and websites. It regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time. Checkmate also has an agent, called [Capture](https://github.com/bluewave-labs/capture), to retrieve data from remote servers. While Capture is not required to run Checkmate, it provides additional insigths about your servers' CPU, RAM, disk and temperature status.
Checkmate is an open source monitoring tool used to track the operational status and performance of servers and websites. It regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time.
Checkmate also has an agent, called [Capture](https://github.com/bluewave-labs/capture), to retrieve data from remote servers. While Capture is not required to run Checkmate, it provides additional insigths about your servers' CPU, RAM, disk and temperature status.
Checkmate has ben stress tested with 1000+ active monitors without any particular issues or performance bottlenecks.
We **love** what we are building here, and we contibuously learn a few things about Reactjs, Nodejs, MongoDB and Docker while building Checkmate.
## 📦 Demo
See [Checkmate](https://checkmate-demo.bluewavelabs.ca/) in action. The username is uptimedemo@demo.com and the password is Demouser1!
See [Checkmate](https://checkmate-demo.bluewavelabs.ca/) in action. The username is uptimedemo@demo.com and the password is Demouser1! (just a note that we update the demo server from time to time, so if it doesn't work for you, please ping us on Discussions channel).
## 🔗 User's guide
Usage instructions can be found [here](https://bluewavelabs.gitbook.io/checkmate).
Usage instructions can be found [here](https://bluewavelabs.gitbook.io/checkmate). It's still WIP and some of the information there might be outdated. Rest assured, we are doing our best! :)
## 🛠️ Installation
See installation instructions in [Checkmate documentation portal](https://bluewavelabs.gitbook.io/checkmate/quickstart).
See installation instructions in [Checkmate documentation portal](https://bluewavelabs.gitbook.io/checkmate/quickstart). Alternatively, you can also use [Coolify](https://coolify.io/) for a one click Docker deployment. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
## 💚 Questions & ideas
We've just launched our [Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions) page! Feel free to ask questions or share your ideas—we'd love to hear from you!
If you have any questions, suggestions or comments, please use our [Discord channel](https://discord.gg/NAb6H3UTjK). We've also launched our [Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions) page! Feel free to ask questions or share your ideas—we'd love to hear from you!
## 🧩 Features
@@ -38,19 +52,29 @@ We've just launched our [Discussions](https://github.com/bluewave-labs/bluewave-
- Page speed monitoring
- Infrastructure monitoring (memory, disk usage, CPU performance etc) - requires [Capture](https://github.com/bluewave-labs/capture)
- Docker monitoring
- Port monitoring
- Ping monitoring
- Incidents at a glance
- E-mail notifications
- Scheduled maintenance
- Can monitor 1000+ servers at the same time on a moderate server
**Short term roadmap:**
- Global (distributed) uptime checking on Solana network
- Status pages
- Better notification options (think Discord, Telegram, Slack)
- More configuration options
- DNS monitoring
- SSL monitoring
- Status pages
- Better notification options
- More configuration options
- Port monitoring
## 🏗️ Screenshots
![Group 3765](https://github.com/user-attachments/assets/8e8144f2-a769-4707-8ea1-99cf758284a8)
![Group 3768](https://github.com/user-attachments/assets/05aed2f2-2cf7-487f-879b-cf8bfb0e9241)
![Group 3768-1](https://github.com/user-attachments/assets/d4ee4bcf-4d69-4e4a-9bce-fd3541129c24)
## 🏗️ Tech stack
@@ -58,33 +82,37 @@ We've just launched our [Discussions](https://github.com/bluewave-labs/bluewave-
- [MUI (React framework)](https://mui.com/)
- [Node.js](https://nodejs.org/en)
- [MongoDB](https://mongodb.com)
- Lots of other open source components!
## 🤝 Contributing
We are known for our great relationship with outside contributors. Checkmate is a young project, but we already have 600+ stars and 30+ contributors from all over the world. The last thing you should do here is to be shy.
We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 2700 stars and attracted almost 40 contributors from around the globe. So, don’t hold back — jump in, contribute and learn with us!
Here's how you can contribute:
- Check [Contributor's guideline](https://github.com/bluewave-labs/bluewave-uptime/blob/master/CONTRIBUTING.md).
- Have a look at our Figma designs [here](https://www.figma.com/design/RPSfaw66HjzSwzntKcgDUV/Uptime-Genie?node-id=0-1&t=WqOFv9jqNTFGItpL-1). We encourage you to copy to your own Figma page, then work on it as it is read-only.
- Open an issue if you believe you've encountered a bug
- Check for good-first-issue's if you are a newcomer
- Make a pull request to add new features/make quality-of-life improvements/fix bugs.
0. Star this repo :)
1. Check [Contributor's guideline](https://github.com/bluewave-labs/bluewave-uptime/blob/master/CONTRIBUTING.md). First timers are encouraged to check `good-first-issue` tag.
2. Optionally, read [project structure](https://bluewavelabs.gitbook.io/checkmate/developers-guide/general-project-structure) and [high level overview](https://bluewavelabs.gitbook.io/checkmate/developers-guide/high-level-overview).
3. Have a look at our Figma designs [here](https://www.figma.com/design/RPSfaw66HjzSwzntKcgDUV/Uptime-Genie?node-id=0-1&t=WqOFv9jqNTFGItpL-1) if you are going to use one of our designs. We encourage you to copy to your own Figma page, then work on it as it is read-only.
4. Open an issue if you believe you've encountered a bug.
5. Check for good-first-issue's if you are a newcomer.
6. Make a pull request to add new features/make quality-of-life improvements/fix bugs.
<a href="https://github.com/bluewave-labs/bluewave-uptime/graphs/contributors">
<img src="https://contrib.rocks/image?repo=bluewave-labs/bluewave-uptime" />
<!-- <a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
</a>
Made with [contrib.rocks](https://contrib.rocks).
-->
![Alt](https://repobeats.axiom.co/api/embed/c35d999c82dbb31e967427ea4166c14da4172e73.svg "Repobeats analytics image")
[![Star History Chart](https://api.star-history.com/svg?repos=bluewave-labs/bluewave-uptime&type=Date)](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
[![Star History Chart](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
Also check other developer and contributor-friendly projects of BlueWave:
- [BlueWave HRM](https://github.com/bluewave-labs/bluewave-hrm)
- [BlueWave Onboarding](https://github.com/bluewave-labs/bluewave-onboarding)
- [BlueWave DataRoom](https://github.com/bluewave-labs/bluewave-dataroom)
- [VerifyWise](https://github.com/bluewave-labs/verifywise)
- [BlueWave DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), an secure file sharing application, aka dataroom.
- [BlueWave HRM](https://github.com/bluewave-labs/bluewave-hrm), a complete Human Resource Management platform.
- [BlueWave Onboarding](https://github.com/bluewave-labs/bluewave-onboarding), an application that helps new users learn how to use your product via hints, tours, popups and banners.
- [VerifyWise](https://github.com/bluewave-labs/verifywise), the first open source AI governance platform.
+4 -3
View File
@@ -281,10 +281,11 @@ const editUser = async (req, res, next) => {
const user = await req.db.getUserByEmail(email);
// Compare passwords
const match = await user.comparePassword(req.body.password);
// If not a match, throw a 401
// If not a match, throw a 403
// 403 instead of 401 to avoid triggering axios interceptor
if (!match) {
const error = new Error(errorMessages.AUTH_INCORRECT_PASSWORD);
error.status = 401;
error.status = 403;
next(error);
return;
}
@@ -360,7 +361,7 @@ const requestRecovery = async (req, res, next) => {
url,
},
email,
"Bluewave Uptime Password Reset"
"Checkmate Password Reset"
);
return res.status(200).json({
+1
View File
@@ -332,6 +332,7 @@ const deleteMonitor = async (req, res, next) => {
await req.db.deleteChecks(monitor._id);
await req.db.deletePageSpeedChecksByMonitorId(monitor._id);
await req.db.deleteNotificationsByMonitorId(monitor._id);
await req.db.deleteHardwareChecksByMonitorId(monitor._id);
} catch (error) {
logger.error({
message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
+5 -1
View File
@@ -124,7 +124,10 @@ import {
//****************************************
// Hardware Checks
//****************************************
import { createHardwareCheck } from "./modules/hardwareCheckModule.js";
import {
createHardwareCheck,
deleteHardwareChecksByMonitorId,
} from "./modules/hardwareCheckModule.js";
//****************************************
// Checks
@@ -213,6 +216,7 @@ export default {
createPageSpeedCheck,
deletePageSpeedChecksByMonitorId,
createHardwareCheck,
deleteHardwareChecksByMonitorId,
createMaintenanceWindow,
getMaintenanceWindowsByTeamId,
getMaintenanceWindowById,
+23 -4
View File
@@ -19,22 +19,41 @@ const createHardwareCheck = async (hardwareCheckData) => {
return null;
}
let newUptimePercentage;
if (monitor.uptimePercentage === undefined) {
monitor.uptimePercentage = status === true ? 1 : 0;
newUptimePercentage = status === true ? 1 : 0;
} else {
monitor.uptimePercentage =
newUptimePercentage =
(monitor.uptimePercentage * (n - 1) + (status === true ? 1 : 0)) / n;
}
await monitor.save();
await Monitor.findOneAndUpdate(
{ _id: monitorId },
{ uptimePercentage: newUptimePercentage }
);
const hardwareCheck = await new HardwareCheck({
...hardwareCheckData,
}).save();
return hardwareCheck;
} catch (error) {
console.log("error creating hardware check", error);
error.service = SERVICE_NAME;
error.method = "createHardwareCheck";
throw error;
}
};
export { createHardwareCheck };
const deleteHardwareChecksByMonitorId = async (monitorId) => {
try {
const result = await HardwareCheck.deleteMany({ monitorId });
console.log("deleted hardware checks", result);
return result.deletedCount;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteHardwareChecksByMonitorId";
throw error;
}
};
export { createHardwareCheck, deleteHardwareChecksByMonitorId };
+29
View File
@@ -0,0 +1,29 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import mochaPlugin from "eslint-plugin-mocha";
/*
Please do not forget to look at the latest eslint configurations and rules.
ESlint v9 configuration is different than v8.
"https://eslint.org/docs/latest/use/configure/"
*/
/** @type {import('eslint').Linter.Config[]} */
export default [
{
languageOptions: {
globals: {
...globals.node, // Add Node.js globals
},
ecmaVersion: 2023,
sourceType: "module",
},
},
pluginJs.configs.recommended, // Core JS rules
mochaPlugin.configs.flat.recommended, // Mocha rules
{
rules: {
"mocha/max-top-level-suites": "warn", // Warn if there are too many top-level suites instead of failing
},
},
];
Loaded 100 of 119 files, more files were not shown because too many files have changed in this diff. Show more