Compare commits

..
567 Commits
Author SHA1 Message Date
Alexander Holliday eb8cf1b6da Merge pull request #2584 from bluewave-labs/develop
bump version
2025-07-08 09:51:08 -07:00
Alex Holliday 1d816b0677 bump version 2025-07-07 16:14:51 -07:00
Alexander Holliday c633a9ad8d Merge pull request #2582 from bluewave-labs/develop
develop -> master
2025-07-07 15:38:46 -07:00
Alexander Holliday 9b413f25fb Merge pull request #2581 from bluewave-labs/feat/delete-notification-dialog
feat: add delete confirmation dialog to notificiaton page
2025-07-07 15:35:23 -07:00
Alex Holliday e08fba1b76 add delete confirmation dialog to notificiaton page 2025-07-07 15:34:45 -07:00
Alexander Holliday 961c398690 Merge pull request #2573 from luismsousa/feat/notifications-delete
Notification channel Delete:
2025-07-07 15:25:55 -07:00
Luis Sousa 9954a74dfd bleep boop fix translations 2025-07-07 20:08:34 +01:00
Alexander Holliday faf4164818 Merge pull request #2563 from bluewave-labs/fix/2471-infrastructure-action-menu
Fix/2471 infrastructure action menu
2025-07-07 11:05:52 -07:00
mohadeseh safari 9254b9096c refactor: Remove default string parameter from t() calls 2025-07-07 12:17:59 -04:00
mohadeseh safari aa8029a854 fix: Update InfrastructureMenu to use isActive instead of status for pause state 2025-07-07 12:16:17 -04:00
mohadeseh safari 9edc6e4b27 Fix formating 2025-07-06 18:11:30 -04:00
mohadeseh safari d842f93a19 fix: Remove unnecessary local monitor state in InfrastructureMenu component 2025-07-06 18:09:17 -04:00
mohadeseh safari 0f82f076f7 Merge branch 'develop' into fix/2471-infrastructure-action-menu 2025-07-06 17:59:51 -04:00
Alexander Holliday 5fdf8431da Merge pull request #2557 from bluewave-labs/fix/placeholder-center-align
Fix/placeholder center align
2025-07-04 14:57:37 -07:00
karenvicent af0c69764f Fix: change height and witdth and Merge branch 'develop' into fix/placeholder-center-align 2025-07-04 17:46:41 -04:00
Alexander Holliday a88e304c5d Merge pull request #2574 from bluewave-labs/feat/fe/resolve-incidents
Feat: Resolve Incidents All and One
2025-07-04 09:58:19 -07:00
Br0wnHammer 847c7630ec Strings localizations 2025-07-04 22:24:59 +05:30
karenvicent 4aaa2faf9a Merge branch 'develop' into fix/placeholder-center-align 2025-07-04 11:15:43 -04:00
Luis Sousa 292c7fd7ea address PR feedback 2025-07-04 11:53:25 +01:00
Gorkem Cetin (BWL) 87263dc6d9 Add Pikapods 2025-07-03 22:17:21 -04:00
Br0wnHammer 8a4fba056b All Incidents Resolve Hook 2025-07-04 01:29:52 +05:30
Br0wnHammer 30131c97a3 Implemented resolve incident hook 2025-07-04 01:25:33 +05:30
Alexander Holliday b02a3379da Merge pull request #2534 from bluewave-labs/issue-2150-move-status-page-delete-button
Move Status Page Delete Button
2025-07-04 02:54:02 +08:00
Alexander Holliday 8f46da0907 Merge pull request #2570 from loma373/feat/if-no-notif
feat: disable send test notification when 0 notifications are configured
2025-07-04 02:24:04 +08:00
Luis Sousa d5262a1968 add i18n for the notifications dropdown menu in the list. 2025-07-02 23:18:34 +01:00
Luis Sousa d9cc0f82c1 add internationalization for button labels in notifications 2025-07-02 23:05:53 +01:00
Luis Sousa 34455e10e4 running npm run format 2025-07-02 22:32:08 +01:00
Br0wnHammer d9ccc72a38 Feat: Resolve Incidents All and One 2025-07-03 02:15:37 +05:30
Amol e2e1eff289 feat: add language localization and refactor to remove duplicate ternary op 2025-07-03 01:26:33 +05:30
Luis Sousa 85b13686a4 Notification channel Delete:
attempt to add the delete button to the notifications view
2025-07-02 17:14:39 +01:00
karenvicent c8bf571f7a implement adjustments and merge with develop 2025-07-02 10:02:01 -04:00
Alexander Holliday 731f2e12fb Merge pull request #2567 from bluewave-labs/feat/fe/incidents-ui
Feat: Status Boxes in Incidents Table
2025-07-02 15:26:51 +08:00
Br0wnHammer 3acfde97df Legacy UI Fixes 2025-07-02 12:51:27 +05:30
Amol 0e8a71c760 fix: removing code duplication 2025-07-01 22:57:28 +05:30
Amol 2e869310b8 feat: disable send test notification when 0 notification configured 2025-07-01 22:44:42 +05:30
Alexander Holliday f5cf8e25f5 Merge pull request #2566 from bluewave-labs/fix/missing-strings
fix: missing strings
2025-07-01 23:47:07 +08:00
Alexander Holliday 9c836ed39b Merge branch 'develop' into fix/missing-strings 2025-07-01 23:45:44 +08:00
Alexander Holliday 32b731f60e Merge pull request #2569 from bluewave-labs/revert-2568-fix/sort-keys
Revert "sort en.json alphabetically"
2025-07-01 23:42:44 +08:00
Alexander Holliday 708234118a Revert "sort en.json alphabetically" 2025-07-01 23:42:31 +08:00
Alexander Holliday 798b9aed8f Merge pull request #2568 from bluewave-labs/fix/sort-keys
sort en.json alphabetically
2025-07-01 23:36:25 +08:00
Alexander Holliday d55196e06f Merge branch 'develop' into fix/sort-keys 2025-07-01 23:36:17 +08:00
Alex Holliday 3d130bd8a5 sort en.json alphabetically 2025-07-01 23:35:11 +08:00
Br0wnHammer 278964500b Minor Changes 2025-07-01 18:23:11 +05:30
Br0wnHammer 644de5f34a Feat: Hooks for Summary 2025-07-01 18:10:35 +05:30
Br0wnHammer 5428999d46 Feat: Summary for Incidents 2025-07-01 17:55:50 +05:30
Br0wnHammer 35a9c081b5 Feat: Status Boxes in Incidents Page 2025-07-01 15:54:50 +05:30
Alex Holliday 3295423804 add settingsSave 2025-07-01 15:27:23 +08:00
Alex Holliday 19c5f4c8c3 update strings 2025-07-01 15:25:45 +08:00
Alex Holliday 6c4717d5f6 resolve strings 2025-07-01 15:23:00 +08:00
karenvicent 1cb730764a Merge branch 'develop' into fix/placeholder-center-align 2025-06-30 12:24:33 -04:00
mohadeseh safari 95403fb56b fix deletion props passed to Tabs component 2025-06-29 12:14:42 -04:00
mohadeseh safari 59c8c14e6c Merge remote-tracking branch 'origin/develop' into issue-2150-move-status-page-delete-button 2025-06-28 19:17:40 -04:00
mohadeseh safari fbbd9b9092 Format MonitorsTableMenu component 2025-06-28 19:09:27 -04:00
mohadeseh safari 1f804a725e Add comment explaining why monitor.type must remain optional in PropTypes 2025-06-28 19:05:21 -04:00
mohadeseh safari 5ba82415ad Format MonitorsTableMenu component 2025-06-28 18:50:14 -04:00
mohadeseh safari c44325ba7f Add missing translation keys for MonitorsTableMenu component 2025-06-28 18:46:17 -04:00
mohadeseh safari 78626253ee Merge develop branch into fix/2471-infrastructure-action-menu 2025-06-28 18:36:36 -04:00
mohadeseh safari 1a2009b615 Fix infrastructure action menu: correct pause/resume text display and handle undefined monitor properties 2025-06-28 17:41:13 -04:00
karenvicent eca4bb0cb5 Fix: center placeholder in content area and merge branch 'develop' into fix/placeholder-center-align 2025-06-27 10:29:28 -04:00
karenvicent 866b29db68 FIx: center placeholde box in the content area 2025-06-27 10:16:37 -04:00
Alexander Holliday 07b2ddc29e Merge pull request #2555 from shanikauwu1/fixed-alighment-issue-on-pagespeed-page-piechart
Fixed alighment issue on pagespeed page piechart
2025-06-27 08:53:48 +08:00
karenvicent 6db0aff073 fix: replace breakpoints with theme.spacing for consistent layout sizing 2025-06-26 14:37:35 -04:00
karenvicent 2bb76a1b2f Merge branch 'develop' into fix/placeholder-center-align 2025-06-26 09:20:48 -04:00
karenvicent e91e01adf3 fix: Hardcoded dimensions in layout constraints with format 2025-06-26 09:18:49 -04:00
shanika Jayawardane c5870b96d5 Fix PageSpeed performance report alignment - Center pie chart and text 2025-06-25 23:19:29 -06:00
Alexander Holliday e772a35243 Merge pull request #2553 from bluewave-labs/hotfix/status-page
apply hotfix for logo
2025-06-26 11:47:16 +08:00
Alex Holliday 6047163150 apply hotfix for logo 2025-06-26 11:46:50 +08:00
Alexander Holliday 4c5bb66c3d Merge pull request #2496 from bluewave-labs/feat/incidents-clear
Feat: Incidents Resolve BE
2025-06-26 09:34:10 +08:00
Alex Holliday d9dbfc6d6e hide resolved 2025-06-26 09:33:39 +08:00
Alexander Holliday b462968650 Merge pull request #2552 from bluewave-labs/fix/action
update msg
2025-06-26 09:26:10 +08:00
Alex Holliday 2c553ae1d2 update msg 2025-06-26 09:25:37 +08:00
Alexander Holliday e04ba2d163 Merge pull request #2547 from deepanshu2711/2543-hide-pause-button-for-members
fix: hide pause button for members (closes #2543)
2025-06-26 09:20:34 +08:00
Alexander Holliday a30417ab42 Merge pull request #2551 from bluewave-labs/hotfix/notification-action-menu
hotfix: stop propagation
2025-06-26 09:15:00 +08:00
Alex Holliday 83373d0520 stop propagation 2025-06-26 09:14:18 +08:00
Alexander Holliday b59cad5389 Merge pull request #2550 from bluewave-labs/hotfix/status-page-with-no-monitors
hotfix: status page
2025-06-26 09:11:35 +08:00
Alex Holliday 58b102b921 fix status page 2025-06-26 09:09:41 +08:00
karenvicent 32edb64da4 fix: Hardcoded dimensions in layout constraints 2025-06-25 20:39:34 -04:00
karenvicent eecdb7e552 fix: center and constrain empty placeholder layout with format 2025-06-25 18:27:37 -04:00
karenvicent fd51af97fa fix: center and constrain empty placeholder layout 2025-06-25 18:06:43 -04:00
Br0wnHammer e5028c95b3 Conflicts in en.json 2025-06-25 14:26:11 +05:30
Aryaman Sharma 5d20cf26ab Merge branch 'develop' into feat/incidents-clear 2025-06-25 14:20:15 +05:30
Br0wnHammer 6457984dd4 ackStage Backword Compatible 2025-06-25 14:16:00 +05:30
Deepanshu 8749edfaf0 fix: hide pause button for members (closes #2543) 2025-06-25 12:52:06 +05:30
Br0wnHammer 78a2e5d106 VerifyTemAccess Middleware 2025-06-25 11:52:38 +05:30
shanika Jayawardane 6ec6d582d8 Merge remote-tracking branch 'upstream/develop' into develop 2025-06-24 23:37:16 -06:00
Alexander Holliday 923eda1307 Merge pull request #2545 from bluewave-labs/develop
develop -> master
2025-06-25 12:42:33 +08:00
Alexander Holliday 6a079704bb Merge pull request #2544 from bluewave-labs/fix/logs-rights
fix: logs access
2025-06-25 12:41:44 +08:00
Alexander Holliday 65eb22bbe0 Merge branch 'develop' into fix/logs-rights 2025-06-25 12:41:30 +08:00
Alex Holliday 549044f973 fix sidebar logs 2025-06-25 12:38:57 +08:00
Alexander Holliday df56799ab9 Merge pull request #2542 from bluewave-labs/fix/process-env
fix: use process.env
2025-06-25 12:17:47 +08:00
Alex Holliday ba8011a22e use process.env 2025-06-25 12:17:16 +08:00
Alexander Holliday 1a69af6360 Merge pull request #2541 from bluewave-labs/fix/fetch-depth
fix: fetch depth
2025-06-25 12:07:58 +08:00
Alex Holliday b718c02787 update workflows 2025-06-25 12:07:31 +08:00
Alex Holliday 4fa0e08574 update workflows 2025-06-25 12:07:12 +08:00
Alexander Holliday 9681730263 Merge pull request #2540 from bluewave-labs/fix/mongo-4.4-compatibility
version fix
2025-06-25 11:57:09 +08:00
Alex Holliday b03f951352 version fix 2025-06-25 11:56:04 +08:00
Alexander Holliday 34b56bcadc Merge pull request #2539 from bluewave-labs/fix/mongo-4.4-compatibility
fix: donwgrade mongo functions
2025-06-25 11:40:30 +08:00
Alex Holliday 8606b5e5de donwgrade mongo functoins 2025-06-25 11:39:05 +08:00
Alexander Holliday 6c7fefe4ed Merge pull request #2538 from bluewave-labs/translation-update-74
🌐 Updated Translations from POEditor
2025-06-25 10:44:55 +08:00
GitHub Action 9bb5362c5f feat: translations updated from POEditor 2025-06-25 02:44:18 +00:00
Alexander Holliday 997342a31c Merge pull request #2536 from bluewave-labs/develop
Develop -> Master for 2.2 release
2025-06-25 09:38:08 +08:00
Alexander Holliday a84f30c36d Merge pull request #2535 from bluewave-labs/fix/invite-email
fix: verify transport before sending email
2025-06-25 09:36:00 +08:00
Alex Holliday 4cc3765e0d verify transport before sending email 2025-06-25 09:35:41 +08:00
Alex Holliday 915d2e06ae verify transport before sending email 2025-06-25 09:34:07 +08:00
Alex Holliday 95b832c1c6 Merge branch 'develop' into feat/incidents-clear 2025-06-25 09:03:00 +08:00
Alexander Holliday 1e0ddb248c Merge pull request #2533 from bluewave-labs/feat/version
feat: extract version from git tag
2025-06-25 08:51:56 +08:00
Alexander Holliday 635717ec92 Merge pull request #2531 from bluewave-labs/fix/settings-page-strings
fix: settings page strings
2025-06-25 08:51:39 +08:00
Alexander Holliday db285d1867 Merge pull request #2530 from bluewave-labs/feat/on-user-delete-cascade
feat: cascade on superuser delete
2025-06-25 08:50:45 +08:00
mohadeseh safari 00ebc4c270 translation fix 2025-06-24 16:29:13 -04:00
mohadeseh safari d1b3d14457 Format code with prettier 2025-06-24 16:02:41 -04:00
mohadeseh safari 6cd52cb31d Merge develop into issue-2150-move-status-page-delete-button 2025-06-24 15:50:34 -04:00
mohadeseh safari 310084a902 translation fix 2025-06-24 15:30:52 -04:00
mohadeseh safari 1b8c6167b7 Add localization for status page creation and update messages 2025-06-24 15:16:24 -04:00
mohadeseh safari e371596d15 Fix status page deletion network requests and implement optimistic UI update pattern 2025-06-24 15:07:37 -04:00
Br0wnHammer 183c13fe53 Naming Changes in BE 2025-06-24 14:44:02 +05:30
Alex Holliday 8d6078e2c1 safely extract version 2025-06-24 16:31:18 +08:00
Alex Holliday 4f17e84247 extract version from git tag 2025-06-24 15:40:50 +08:00
Alex Holliday 563cc8d03d fix label 2025-06-24 14:45:04 +08:00
Alex Holliday 09b5261ba2 Merge branch 'develop' into fix/settings-page-strings 2025-06-24 14:42:39 +08:00
Alexander Holliday ff9a7c33eb Merge pull request #2532 from bluewave-labs/fix/sort
sort json
2025-06-24 14:38:03 +08:00
Alex Holliday 08b9cc16fc sort json 2025-06-24 14:37:16 +08:00
Alexander Holliday 27c1e9f1da Merge pull request #2518 from tamirazrab/refactor/2511-remove-useless-string
chore: remove unnecessary string
2025-06-24 14:34:53 +08:00
Alexander Holliday 8d0cd1dc05 Merge pull request #2522 from bluewave-labs/feat/uptime-empty-view
feat: add stat boxes and timeframe header
2025-06-24 14:34:25 +08:00
Alex Holliday 0d01a08304 sort 2025-06-24 14:32:28 +08:00
Alex Holliday 4d91377259 fix settings page strings 2025-06-24 14:30:14 +08:00
Tamir Azrab fe25a5d170 chore: remove keys entirely 2025-06-24 10:44:35 +05:00
Alex Holliday c16d5c4246 early return if user not found 2025-06-24 13:25:10 +08:00
Alex Holliday 40f4294cae cascade on superuser delete 2025-06-24 12:07:02 +08:00
Alexander Holliday 1cc9f4e4d9 Merge pull request #2529 from bluewave-labs/hotfix/delete-user
hotfix: delete user
2025-06-24 11:46:51 +08:00
Alex Holliday b282ff9883 fix delete user 2025-06-24 11:45:51 +08:00
Alexander Holliday 06d35e1626 Merge pull request #2528 from bluewave-labs/hotfix/logger
hotfix: add missing contenxt
2025-06-24 11:19:23 +08:00
Alex Holliday d184f3fd38 add missing contenxt 2025-06-24 11:18:13 +08:00
Alexander Holliday 3d67cfaacd Merge pull request #2527 from bluewave-labs/hotfix/logger
add missing  context
2025-06-24 11:15:48 +08:00
Alex Holliday 2ee2d9b905 add missing context 2025-06-24 11:15:14 +08:00
Alexander Holliday 2df8bc14dd Merge pull request #2526 from bluewave-labs/feat/update-docker-compose
feat: update docker compose
2025-06-24 11:13:58 +08:00
Alex Holliday 2cb40b3885 update docker ignore 2025-06-24 11:12:44 +08:00
Alex Holliday 536c27cb30 update docker compose files 2025-06-24 10:59:01 +08:00
Alexander Holliday 1df36a66b9 Merge pull request #2525 from bluewave-labs/fix/temporary-comment-out-import-export
fix: temporarliy comment out import/export
2025-06-24 10:52:48 +08:00
Alex Holliday 1ebb6b922b temporarliy comment out import/export 2025-06-24 10:52:19 +08:00
Alexander Holliday dc7cc431d2 Merge pull request #2524 from bluewave-labs/fix/i18n
fix: translations ignore empty strings
2025-06-24 10:45:33 +08:00
Alex Holliday b9a8328a48 ignore empty strings 2025-06-24 10:44:16 +08:00
Alexander Holliday 5e14c15b92 Merge pull request #2523 from bluewave-labs/fix/bg-color
fix: update bg color
2025-06-24 10:29:02 +08:00
Alex Holliday 3bcf3b1ed0 update bg color 2025-06-24 10:28:25 +08:00
Alex Holliday e7429ecd67 add stat boxes and timeframe header 2025-06-24 10:18:25 +08:00
Alexander Holliday 0c94472062 Merge pull request #2520 from bluewave-labs/fix/prod-docker-compose
fix: add entrypoint to prod certbot
2025-06-24 10:09:52 +08:00
Alex Holliday dbf9f86105 add entrypoint to prod certbot 2025-06-24 10:09:07 +08:00
Alexander Holliday dfee81f5b0 Merge pull request #2517 from bluewave-labs/feat/team-access-middleware
feat: add middleware for verifying team access
2025-06-24 10:03:00 +08:00
Alex Holliday 40f879f671 user and teamId check 2025-06-24 10:02:36 +08:00
Alex Holliday c8685c4273 remove console 2025-06-24 10:00:41 +08:00
Alexander Holliday 2b9fbf743a Merge pull request #2519 from Giridhar88/feature/maintenance-window-click
Add maintenance row click feature to edit the maintenance window
2025-06-24 09:11:05 +08:00
Br0wnHammer 30d85e5712 Ack Field in Checks 2025-06-23 22:07:03 +05:30
giridhar88 ac9ff86e68 Add maintenance row click feature to edit the maintenance window 2025-06-23 19:01:03 +05:30
Alexander Holliday b6680be345 Merge pull request #2513 from bluewave-labs/feat/queue-page-update
feat: move queue to logs page
2025-06-23 20:10:46 +08:00
Alex Holliday 7839e4c197 correct logging 2025-06-23 20:10:10 +08:00
Alexander Holliday daa57ed948 Merge pull request #2509 from bluewave-labs/feat/email-settings-transport-display
feat: display transport settings
2025-06-23 19:05:16 +08:00
Tamir Azrab 41137c09b2 chore: remove unnecessary string 2025-06-23 15:52:34 +05:00
Alex Holliday 05add10ba8 add middleware for verifying team access 2025-06-23 15:45:24 +08:00
Alex Holliday bf73a71523 remove duplicate user 2025-06-23 14:48:45 +08:00
Alexander Holliday 91ab821090 Merge pull request #2506 from JefferMarcelino/feature/hide-admin-login-link
Add toggle to optionally display admin login link on status page and fix syntax errors
2025-06-23 13:25:43 +08:00
Alex Holliday 32688ba320 add missing stack 2025-06-23 13:22:53 +08:00
Alexander Holliday 288c6f88a3 Merge pull request #2515 from bluewave-labs/feat/logs
feat: logs
2025-06-23 13:19:52 +08:00
Alex Holliday 0336170c8d refactor 2025-06-23 13:19:18 +08:00
Alex Holliday 03ced27d30 Merge branch 'develop' into feat/queue-page-update 2025-06-23 13:15:14 +08:00
Alexander Holliday 8e4bb89974 Merge pull request #2516 from bluewave-labs/fix/light-mode-bg
fix: don't render bg svg on light mode
2025-06-23 12:35:31 +08:00
Alex Holliday 35aa5162ca don't render bg svg on light mode 2025-06-23 12:34:30 +08:00
Alex Holliday 92c2b2d6c1 remove unused import 2025-06-23 12:26:35 +08:00
Alex Holliday 5758a84e13 fix tab order 2025-06-23 12:21:27 +08:00
Alex Holliday 176f576979 change page order 2025-06-23 12:20:33 +08:00
Alex Holliday 9d18d55bbf increase log cache size 2025-06-23 12:19:52 +08:00
Alex Holliday 961d827a52 add a description 2025-06-23 12:19:40 +08:00
Alex Holliday 365ae08123 FE log component implementation 2025-06-23 12:09:30 +08:00
Alex Holliday 0b6febbb82 add log cache, routes, and controller 2025-06-23 11:25:42 +08:00
Alexander Holliday 6913bd0179 Merge pull request #2514 from bluewave-labs/translation-update-73
🌐 Updated Translations from POEditor
2025-06-23 11:14:12 +08:00
GitHub Action c607581767 feat: translations updated from POEditor 2025-06-23 03:11:03 +00:00
Alex Holliday 34485a0cae move queue to logs 2025-06-23 11:07:12 +08:00
Alexander Holliday 05e2bbd211 Merge pull request #2508 from bluewave-labs/fix/update-prod-docker-compose
remove redis from production docker copose
2025-06-23 10:06:24 +08:00
Alex Holliday 766c073ec4 localisation 2025-06-23 10:05:06 +08:00
Alex Holliday 3faa6194f3 display transport settings 2025-06-23 10:02:06 +08:00
Alex Holliday ddf9673a35 remove redis from production docker copose 2025-06-23 09:24:33 +08:00
Jeffer Marcelino 5fcf8ce7ba chore: format 2025-06-23 02:58:51 +02:00
Alexander Holliday d10341ac2f Merge pull request #2489 from Owaiseimdad/network-page-BE-part-1
Code for network db settings
2025-06-23 08:52:08 +08:00
Alexander Holliday 2161ef4388 Merge pull request #2492 from bluewave-labs/fix/notifications-loading-state
fix: button loading state
2025-06-23 08:49:15 +08:00
Alexander Holliday 7870e83586 Merge branch 'develop' into feature/hide-admin-login-link 2025-06-23 08:45:37 +08:00
Alexander Holliday 230753320a Merge pull request #2504 from Sky-walkerX/refactor/email-settings-toggle
refactor(settings): moved email settings toggle buttons to end of line and renamed the wordings
2025-06-23 08:43:38 +08:00
Jeffer Marcelino 465bdd2945 chore: format 2025-06-22 18:33:58 +02:00
Jeffer Marcelino 4a36bd3157 feat: add toggle to show/hide “Administrator? Login Here” link in status page settings 2025-06-22 18:28:17 +02:00
Jeffer Marcelino e6fd57d0bb feat: optionally hide the "Administrator? Login Here" on status page 2025-06-22 18:27:56 +02:00
Jeffer Marcelino 7c5516cd75 feat(api): add backend support for toggling admin login link visibility on status page 2025-06-22 18:26:20 +02:00
Jeffer Marcelino 31321749b2 fix: syntax errors in NetworkService.js and en.json causing build failure 2025-06-22 17:11:01 +02:00
Sky-walkerX 50e9a6b967 chore: formatted NetworkService 2025-06-22 19:40:42 +05:30
Sky-walkerX 3a6d3071be chore: format 2025-06-22 17:39:42 +05:30
Sky-walkerX 9972c3cf9a refactor(settings): changed wordings of email settings 2025-06-22 17:35:45 +05:30
Sky-walkerX f853a03491 refactor(settings): email settings toggle in one row 2025-06-22 17:17:26 +05:30
Alexander Holliday c98b901902 Merge pull request #2495 from bluewave-labs/feat/dev-settings
feat: dev settings
2025-06-21 18:00:34 +08:00
Alexander Holliday b77f05709b Merge branch 'develop' into feat/dev-settings 2025-06-21 18:00:23 +08:00
Alexander Holliday 28157c0b55 Merge pull request #2475 from Owaiseimdad/Bulk-export-uptime-monitors
Addition of export.
2025-06-21 17:57:42 +08:00
Alex Holliday f3072e0447 add missing columns 2025-06-21 16:20:31 +08:00
Alex Holliday 105353c9f6 add factory utils for creating header function 2025-06-21 16:15:34 +08:00
Alexander Holliday 40117f777e Merge pull request #2502 from bluewave-labs/fix/missing-password-error-translation
Fix Missing Password Error Translation
2025-06-21 15:29:33 +08:00
mohadeseh safari 2523d73625 Format code with prettier 2025-06-20 20:58:36 -04:00
mohadeseh safari 74cef5adb1 Fix #2471: Add Details, Configure, and Pause options to infrastructure action menu 2025-06-20 20:39:18 -04:00
mohadeseh safari 452d1a2ad5 Fix missing password error translation for auth.common.fields.password.errors.incorrect 2025-06-20 19:58:11 -04:00
mohadeseh safari a29282801a Format code for PR submission (#2150) 2025-06-20 19:06:57 -04:00
mohadeseh safari fc1f4fc142 Modify 2025-06-20 18:59:50 -04:00
mohadeseh safari 2b017490a3 Move status page delete button to configuration page header (#2150) 2025-06-20 18:56:39 -04:00
Owaise Imdad ecb1ca4517 Missed to add a line in jsdoc. 2025-06-21 00:25:57 +05:30
Owaise Imdad ba00e9afff Added JSDocs in module file. 2025-06-21 00:25:30 +05:30
Owaise Imdad 9700551f41 Removal of n count of document. 2025-06-21 00:18:09 +05:30
ThetaOI 24cb4c70a7 Merge branch 'bluewave-labs:develop' into network-page-BE-part-1 2025-06-21 00:09:51 +05:30
Owaise Imdad 75ed049b01 Resolved teh conflict in en.json file. 2025-06-21 00:06:09 +05:30
Br0wnHammer faa2a2d965 Fixes in the controllers 2025-06-20 20:42:57 +05:30
Br0wnHammer a9c1d131a5 Route Update All Monitor/Team Checks 2025-06-20 16:11:22 +05:30
Br0wnHammer e8908351a2 Route for Updating a single Incident Status 2025-06-20 15:39:13 +05:30
Alex Holliday 3351115e3c fix props 2025-06-20 14:47:19 +08:00
Alex Holliday 2b279af093 localization 2025-06-20 14:30:17 +08:00
Alex Holliday a7f795795f add route 2025-06-20 14:29:36 +08:00
Alex Holliday 85a996635f add controller method for getting all metrics 2025-06-20 14:29:27 +08:00
Alex Holliday 92d4e9ea12 Add additional metrics 2025-06-20 14:29:08 +08:00
Alex Holliday c23e941ece Add queue hooks 2025-06-20 14:28:43 +08:00
Alex Holliday c804f8b873 add routes and localisation 2025-06-20 14:28:33 +08:00
Alex Holliday 6c51fc91b4 add dev settings 2025-06-20 14:28:20 +08:00
Alex Holliday e610acb9c7 Add Queue page and components 2025-06-20 14:27:50 +08:00
Alex Holliday d8573be7ac add util methods 2025-06-20 14:27:22 +08:00
Alex Holliday 0a0c84b7f4 update link component to allow for internal links 2025-06-20 14:26:59 +08:00
Alex Holliday 61a66644da disable job on addition if monitor is paused 2025-06-20 10:58:29 +08:00
Alex Holliday 1187f9d4cc add fix button loading state 2025-06-20 10:08:13 +08:00
Alexander Holliday 906a76c545 Merge pull request #2491 from bluewave-labs/refactor/notification-service
refactor: notification service, resolves #2467, resolves #2468
2025-06-20 09:04:01 +08:00
Br0wnHammer 205e16a96d Resolved Filter in FE 2025-06-20 02:54:09 +05:30
Owaise Imdad 4cf3804898 Formatting issues. 2025-06-19 22:45:04 +05:30
Owaise Imdad 223203737f Logging error only once as middleware takes care of it. 2025-06-19 22:42:07 +05:30
ThetaOI 5675990504 Merge branch 'bluewave-labs:develop' into network-page-BE-part-1 2025-06-19 22:36:57 +05:30
Owaise Imdad 9f88fbbcf2 Formatting issues. 2025-06-19 22:32:53 +05:30
Owaise Imdad c70b5b0ae8 Resolving merge issues. 2025-06-19 22:30:50 +05:30
Owaise Imdad a1c11fbb3c Added a new method for file download and correct csv is getting added. 2025-06-19 22:23:10 +05:30
Alex Holliday 2b48a72f35 formatting 2025-06-19 12:17:00 +08:00
Alex Holliday 48b25e0838 refactor notification service 2025-06-19 12:12:55 +08:00
Alex Holliday 1b4b66e36d inject notification utils 2025-06-19 12:12:34 +08:00
Alex Holliday 30a7de5333 remove unused import 2025-06-19 12:12:09 +08:00
Alex Holliday d4b8eebd64 cleanup 2025-06-19 12:11:43 +08:00
Alex Holliday 8ea2053d37 add a utils file for notifications 2025-06-19 12:11:11 +08:00
Alex Holliday 68aa9a6b2d remove unused route 2025-06-19 12:10:44 +08:00
Alex Holliday dbbe008e23 remove unused controller methods 2025-06-19 12:10:29 +08:00
Alex Holliday 7e5521eb13 Move thresholds from Notification to Monitor 2025-06-19 12:10:07 +08:00
Alex Holliday f9e0930336 Merge branch 'develop' into refactor/notification-service 2025-06-19 10:34:14 +08:00
Alexander Holliday dfbc3ed728 Merge pull request #2486 from bluewave-labs/refactor/build-and-send-email
refactor: build and send
2025-06-19 10:33:37 +08:00
Alex Holliday 7ed92427a3 fix error handling 2025-06-19 10:33:00 +08:00
Alexander Holliday 2df1dc1721 Merge pull request #2469 from bluewave-labs/refactor/useFetchChecks
Refactor: useFetchChecks
2025-06-19 09:51:26 +08:00
Alexander Holliday 9a88523ea5 Merge pull request #2481 from virendra-jadhav/feat/bulk-select-status-page-servers
Add bulk selection for status page servers
2025-06-19 09:48:26 +08:00
Alex Holliday 196757d36f inject logger 2025-06-19 09:44:24 +08:00
Alexander Holliday 28718680fa Merge pull request #2473 from deepanshu2711/fix/floating-strings-2470
fix(ui): shorten URL label and resize notification button (#2470)
2025-06-19 07:23:25 +08:00
Owaise Imdad 8fe3ed9bd1 Added formating. 2025-06-18 23:30:39 +05:30
Owaise Imdad 0f298d6450 Code for network db settings. 2025-06-18 23:23:24 +05:30
unknown 8d93d80e1e resolve merge conflit for bulk selection for status page servers 2025-06-18 22:32:14 +05:30
Owaise Imdad e6b9b5df3c Translation and localisation. 2025-06-18 22:31:48 +05:30
unknown 711cd6cd44 resolve merge conflit for bulk selection for status page servers 2025-06-18 22:29:13 +05:30
Owaise Imdad 81d830d99e Miss with respect to blob type. 2025-06-18 22:24:43 +05:30
Virendra Jadhav b041c9014f Merge branch 'develop' into feat/bulk-select-status-page-servers 2025-06-18 22:07:49 +05:30
myusername 3cacda687c fix more changes for bulk selection on status page server 2025-06-18 19:05:04 +05:30
Alex Holliday 7bca60269c update schema and validation 2025-06-18 14:42:59 +08:00
Alex Holliday bb921690bf Update validation 2025-06-18 14:42:39 +08:00
Alex Holliday 440ce8a12b simplify creatign notifications 2025-06-18 14:42:26 +08:00
Alex Holliday a86bc59e09 remove unsued params from hook 2025-06-18 14:42:00 +08:00
Br0wnHammer a091bf23b7 isLoading Check 2025-06-18 11:32:33 +05:30
Br0wnHammer acd5dc982e Fix: Type Check in useFetchChecksMonitor 2025-06-18 11:13:42 +05:30
Deepanshu 6b3773ed76 chore: remove URL_TRUNCATE_LENGTH from component 2025-06-18 11:09:23 +05:30
Alex Holliday 29cfc4fb41 Merge branch 'develop' into refactor/notification-service 2025-06-18 13:30:24 +08:00
Alexander Holliday 30e95b431c Merge pull request #2488 from bluewave-labs/hotfix/fetch-settings
fix: add empty functions
2025-06-18 13:20:56 +08:00
Alex Holliday cb4fda937f add empty functions 2025-06-18 13:20:08 +08:00
Alexander Holliday 728e337722 Merge pull request #2487 from bluewave-labs/hotfix/react-router
hotfix: react router import
2025-06-18 10:55:07 +08:00
Alex Holliday d558cdd49f fix react router import 2025-06-18 10:52:11 +08:00
Alex Holliday 47131633d0 refactor build and send 2025-06-18 10:44:59 +08:00
Alexander Holliday 85e81184e5 Merge pull request #2485 from bluewave-labs/hotfix/radio
hotfix: add name prop to radio component
2025-06-18 10:10:57 +08:00
Alex Holliday c78f43e32e add name prop to radio component 2025-06-18 10:10:14 +08:00
Alexander Holliday 79b5a33874 Merge pull request #2484 from bluewave-labs/hotfix/ping-monitoring
hotfix: ping monitoring
2025-06-18 10:06:18 +08:00
Alex Holliday 76a5a79482 fix ping monitoring 2025-06-18 10:05:04 +08:00
Alexander Holliday f45736e318 Merge pull request #2483 from hackr-sh/fix/first-name-last-name-additional-special-chars
fix: name validation now takes into account additional special chars
2025-06-18 09:55:39 +08:00
Alexander Holliday 8d30de1ff8 Merge pull request #2480 from bluewave-labs/fix/time-format
fix: update time format function, resolves #2477
2025-06-18 09:44:20 +08:00
Alexander Holliday 68f1895cda Merge pull request #2478 from bluewave-labs/feat/handle-api-and-password-input
feat: handle api and password input, resolves #2385
2025-06-18 09:43:59 +08:00
Alex Holliday fc2676c33e deal with redux before validation 2025-06-18 09:43:41 +08:00
Mohammad Al-Ahdal c9359cc4e1 implement server-validation wording feedback 2025-06-17 18:40:51 -06:00
Mohammad Al-Ahdal b5752024e7 fix: name validation now takes into account additional special chars 2025-06-17 18:29:09 -06:00
Gorkem Cetin (BWL) df4f55b410 Update README.md 2025-06-17 19:38:08 -04:00
Owaise Imdad e71e2ef42c Addressed all the comments in pr. 2025-06-18 01:07:22 +05:30
Gorkem Cetin (BWL) 1e5c8612b8 Sponsor a feature link added 2025-06-17 14:59:38 -04:00
Deepanshu 75309fc53c chore: add URL_TRUNCATE_LENGTH 2025-06-17 17:36:06 +05:30
Deepanshu e3b6ed27ba chore: remove nowrap as already exists in monitorUrl varient 2025-06-17 17:33:21 +05:30
Deepanshu f474d63e88 chore: extract monitor URL formatting to utils 2025-06-17 17:30:11 +05:30
Br0wnHammer 287ea3e5a6 Updated params 2025-06-17 13:27:51 +05:30
Br0wnHammer bcf883223b Exporting Hooks from One File 2025-06-17 13:13:21 +05:30
Alex Holliday 9c40021318 return time string 2025-06-17 13:41:25 +08:00
Deepanshu e818f1cd61 chore: fix formatting 2025-06-17 10:57:30 +05:30
unknown 34ca3a65d6 Add bulk selection for status page servers 2025-06-17 10:13:58 +05:30
Alex Holliday 1298b23844 update time format function 2025-06-17 11:32:35 +08:00
Alexander Holliday f15cef4916 Merge pull request #2479 from bluewave-labs/translation-update-72
🌐 Updated Translations from POEditor
2025-06-17 11:17:04 +08:00
GitHub Action 64e4a3278a feat: translations updated from POEditor 2025-06-17 03:15:12 +00:00
Alex Holliday 791c62b583 remove console log 2025-06-17 11:13:37 +08:00
Alex Holliday 456798ca15 move state to parent 2025-06-17 10:58:41 +08:00
Alex Holliday 7d333c6d93 return updated settings 2025-06-17 10:58:30 +08:00
Alexander Holliday 7c275db662 Merge pull request #2466 from bluewave-labs/fix/hardware-notifications
fix: hardware notifications
2025-06-17 09:27:34 +08:00
Alexander Holliday c1004fc37a Merge pull request #2458 from bluewave-labs/feat/discord-notification-channel
feat: discord notification channel
2025-06-17 09:21:36 +08:00
Owaise Imdad 11212c7371 Formatting was done. 2025-06-17 00:17:46 +05:30
Owaise Imdad e9e509ab04 Addition of export. 2025-06-17 00:11:33 +05:30
Deepanshu df42932e0c fix(ui): shorten URL label and resize notification button (#2470) 2025-06-16 20:40:19 +05:30
Gorkem Cetin (BWL) 36d8e8c0cf Better screnshots 2025-06-16 09:46:50 -04:00
Gorkem Cetin (BWL) 75eb9f8560 Add ToC 2025-06-16 09:43:59 -04:00
Br0wnHammer c981b25ef9 Hooks integration in Incidents Table 2025-06-16 13:51:15 +05:30
Br0wnHammer 638fcb3b9b Incidents useFetchChecksMonitor Integration 2025-06-16 13:08:39 +05:30
Br0wnHammer 37406484fc useFetchChecksTeam Hook 2025-06-16 13:00:23 +05:30
Br0wnHammer bebbe5896d Uptime useFetchChecksByMonitor Integration 2025-06-16 12:57:00 +05:30
Br0wnHammer f47d3c2cd6 Feat: useFetchChecksByMonitor 2025-06-16 12:56:08 +05:30
Alex Holliday 3df6b451e7 fix hardware notifications 2025-06-16 13:53:08 +08:00
Alex Holliday 4fd2a29ab3 fix email type 2025-06-16 13:30:27 +08:00
Alexander Holliday a3b22d9056 Merge pull request #2465 from bluewave-labs/fix/send-test-notifications
fix: send test notifications
2025-06-16 13:28:27 +08:00
Alex Holliday ca4da6d485 add testAll 2025-06-16 12:17:43 +08:00
Alex Holliday 959f265617 add route 2025-06-16 12:17:22 +08:00
Alex Holliday 3cdc21f1be add controller method 2025-06-16 12:16:52 +08:00
Alex Holliday bc3801edc0 add strings' 2025-06-16 12:16:08 +08:00
Alex Holliday 13741d0f0a add test all notifications method 2025-06-16 12:15:54 +08:00
Alex Holliday 9bba8440ca update monitor details header 2025-06-16 12:15:29 +08:00
Alex Holliday 6f133ed7a0 add test all hook 2025-06-16 12:15:07 +08:00
Alexander Holliday 55ea81c46a Merge pull request #2464 from bluewave-labs/fix/cleanup
fix: cleanup
2025-06-16 11:36:52 +08:00
Alexander Holliday 37443823b8 Merge pull request #2463 from bluewave-labs/fix/incidents
fix: load monitor from params if present
2025-06-16 11:35:40 +08:00
Alexander Holliday 7c76125a0a Merge pull request #2462 from bluewave-labs/fix/infrastructure-header
fix: add universal header to infra details
2025-06-16 11:35:20 +08:00
Alexander Holliday dd16c3e8fe Merge pull request #2461 from bluewave-labs/fix/pagespeed-header
fix: pagespeed header
2025-06-16 11:35:04 +08:00
Alex Holliday 30a610511c remove unused component/imports 2025-06-16 11:34:41 +08:00
Alex Holliday e173269acd remove unused depedencies 2025-06-16 11:34:25 +08:00
Alex Holliday 3235bf68e4 Merge branch 'fix/incidents' into fix/cleanup 2025-06-16 11:24:36 +08:00
Alex Holliday 6cbc6546ac Merge branch 'fix/infrastructure-header' into fix/cleanup 2025-06-16 11:24:28 +08:00
Alex Holliday 1490e9171d load monitor from params if present 2025-06-16 11:21:28 +08:00
Alex Holliday f30529e8a2 add universal header to infra details 2025-06-16 11:17:09 +08:00
Alex Holliday 90a5fe946a add update trigger prop 2025-06-16 11:13:45 +08:00
Alex Holliday 1a198c28ee use universal details header in PageSpeed page 2025-06-16 11:13:28 +08:00
Alexander Holliday c90fb9dd08 Merge pull request #2460 from bluewave-labs/hotfix/uptime-config
hotfix: uptime config
2025-06-16 10:45:52 +08:00
Alex Holliday 9de6b4e2bc fix uptime config 2025-06-16 10:45:13 +08:00
Alex Holliday bea23a94bb handle discord webhooks 2025-06-16 10:38:05 +08:00
Alex Holliday 5aae164865 extrat userId and teamId from req.uesr 2025-06-16 10:37:04 +08:00
Alex Holliday bfae00b69b add strings 2025-06-16 10:36:45 +08:00
Alex Holliday 22f8a37f1f add discord type 2025-06-16 10:36:34 +08:00
Alex Holliday f6be4317cd remove user and teamid from validation 2025-06-16 10:36:20 +08:00
Alexander Holliday 1aeb4472bc Merge pull request #2457 from bluewave-labs/hotfix/rows-per-page
hotfix: safe rowsPerPage access
2025-06-16 09:45:27 +08:00
Alex Holliday 20e32fad04 safe rowsPerPage access 2025-06-16 09:44:21 +08:00
Alexander Holliday cb35dc591c Merge pull request #2450 from loma373/feat/search-infra-moniters
feat: add search functionality to infrastructure monitor
2025-06-15 20:44:55 +08:00
Amol 738925d5ec fix: remove shouldRender prop as it is now managed by isLoading 2025-06-15 15:29:48 +05:30
Alexander Holliday b229720431 Merge pull request #2455 from bluewave-labs/feat/image-tags
fix: change workflow names
2025-06-15 17:36:02 +08:00
Alexander Holliday beffce8823 Merge pull request #2453 from Kaotic3/feature/persist-rows-setting
Feature/persist rows setting
2025-06-15 17:12:21 +08:00
Alexander Holliday 1607aba78f Merge pull request #2427 from bluewave-labs/feat/test-email
Feat: New Email Options
2025-06-15 17:11:12 +08:00
Amol Vyas 87ccc93ff9 Merge branch 'develop' into feat/search-infra-moniters 2025-06-15 03:39:10 +05:30
Amol 91741c2ae5 fix: removing unused code blocks and better handling for filter param 2025-06-15 03:17:22 +05:30
Amol e366741aa9 fix: updating code to the coding standards & changing the search param 2025-06-14 23:49:39 +05:30
Steven Kennedy b105626f0b Persist rowsPerPage setting in local storage 2025-06-14 17:40:46 +01:00
Kaotic3 a37505b095 Merge pull request #1 from Kaotic3/codex/persist-rowsperpage-setting
Persist infrastructure rows per page
2025-06-14 17:16:26 +01:00
Kaotic3 216e53166d Persist infrastructure monitors rows per page 2025-06-14 17:13:55 +01:00
Amol 800fd4792d fix: running formatting script on both client and server directories 2025-06-14 16:23:05 +05:30
Amol 7189c19d55 feat: add search functionality to infrastructure moniter 2025-06-14 16:16:49 +05:30
Br0wnHammer 91694ff072 Password Validation Check 2025-06-14 11:01:47 +05:30
Alexander Holliday fcffb93a60 Merge pull request #2449 from bluewave-labs/fix/status-page-on-monitor-delete
fix/status page on monitor delete
2025-06-14 11:41:41 +08:00
Alexander Holliday ad7912e6ba Merge pull request #2448 from bluewave-labs/fix/bulk-create
fix: move bulk monitors hook to monitorHooks
2025-06-14 11:41:25 +08:00
Alexander Holliday 252961aca9 Merge pull request #2447 from bluewave-labs/fix/autocomplete-style
fix: move style to global theme
2025-06-14 11:41:07 +08:00
Br0wnHammer 2d839285f2 Switch Buttons Position 2025-06-13 20:40:01 +05:30
Br0wnHammer d19bbf2b8e Hide send email button until validation is complete 2025-06-13 14:15:37 +05:30
Alex Holliday 4b621a228f remove monitors from status page on delete 2025-06-13 14:54:59 +08:00
Br0wnHammer 38a1d5e7a1 Implement PR Comments 2025-06-13 12:22:28 +05:30
Alex Holliday cc8949538f move bulk monitors hook to monitorHooks 2025-06-13 14:04:08 +08:00
Alex Holliday 985ca3334d move style to global theme 2025-06-13 13:49:54 +08:00
Alexander Holliday df69d06048 Merge pull request #2446 from bluewave-labs/fix/remove-team-and-user-id
fix: remove team and user
2025-06-13 13:35:36 +08:00
Alexander Holliday ef8f74d4e3 Merge pull request #2445 from bluewave-labs/fix/checks-team-id
fix: checks team
2025-06-13 13:34:35 +08:00
Alexander Holliday 062df395c5 Merge pull request #2444 from bluewave-labs/fix/maintenance-window-team-id
fix: maintenance window team
2025-06-13 13:34:22 +08:00
Alexander Holliday a25f292217 Merge pull request #2443 from bluewave-labs/fix/monitors-remove-team-id
fix: monitors remove team ID parameters
2025-06-13 13:34:05 +08:00
Alexander Holliday 30fc14ed7f Merge pull request #2442 from bluewave-labs/fix/notifications-team-id
fix: remove team id from notifications
2025-06-13 13:33:48 +08:00
Alexander Holliday 1850655467 Merge pull request #2441 from bluewave-labs/fix/advanced-settings
fix: add a checkbox to enable/disable advanced settings
2025-06-13 13:33:30 +08:00
Alex Holliday ab3f867b5b remove unused redux state 2025-06-13 13:32:55 +08:00
Alex Holliday b499b2879c remove teamId from status pages 2025-06-13 13:29:03 +08:00
Alex Holliday 18ea551118 remove user 2025-06-13 13:11:58 +08:00
Alex Holliday 42372ab111 remove userID and teamID from status page 2025-06-13 13:10:56 +08:00
Alex Holliday 5e23a2b79c Remove team and user id 2025-06-13 12:16:52 +08:00
Alex Holliday c32cde6189 fix incidents page 2025-06-13 12:08:59 +08:00
Alex Holliday d2233d2e3e remove teamId from checks 2025-06-13 12:04:39 +08:00
Alex Holliday 9da3d3ef55 remove teamId from checks 2025-06-13 12:03:32 +08:00
Alex Holliday cedd660fd9 get teamID from req.user 2025-06-13 11:52:10 +08:00
Alex Holliday b9fbfd2a25 remove team id from monitor controller/db methods, standardize parameters 2025-06-13 11:49:03 +08:00
Alex Holliday 4168c86734 remove team id from monitor controller/db methods, standardize parameters 2025-06-13 11:47:29 +08:00
Alex Holliday 024096631f remove team id from notificaitons 2025-06-13 11:15:42 +08:00
Alex Holliday e53901f6fd add a checkbox to enable/disable advanced settings 2025-06-13 11:07:37 +08:00
Alexander Holliday 1a7842164d Merge pull request #2434 from bluewave-labs/fix/redux-removal-action-menu
fix: remove redux from action menu
2025-06-13 10:55:19 +08:00
Alexander Holliday 656c3c61a8 Merge pull request #2435 from bluewave-labs/fix/remove-redux-sidebar
fix: remove redux sidebar
2025-06-13 10:54:32 +08:00
Alexander Holliday 19303d43ea Merge pull request #2436 from bluewave-labs/fix/redux-removal-password-reset
fix: redux removal password reset
2025-06-13 10:54:15 +08:00
Alexander Holliday cd0b9cd7d6 Merge pull request #2437 from bluewave-labs/fix/redux-removal-settings
fix: remove redux from settings
2025-06-13 10:54:02 +08:00
Alexander Holliday 5e8a0afd3b Merge pull request #2438 from bluewave-labs/fix/remove-uptime-reducer
fix: remove uptime reducer
2025-06-13 10:53:50 +08:00
Alexander Holliday fac2f6f205 Merge pull request #2439 from bluewave-labs/fix/remove-unused-reducers
fix: remove unused reducers
2025-06-13 10:53:34 +08:00
Alexander Holliday c3a2ca314a Merge pull request #2440 from bluewave-labs/fix/remove-team-id-from-requests
fix: remove team id from requests
2025-06-13 10:53:18 +08:00
Alex Holliday 57f4db8d37 remove reference to state 2025-06-13 10:52:05 +08:00
Alex Holliday afece4df65 update routes, validation 2025-06-13 10:46:06 +08:00
Alex Holliday d9c2f31f4e remove teamId from uptime monitor requests 2025-06-13 10:38:52 +08:00
Alex Holliday 134e547736 remove redux loading state 2025-06-13 10:18:43 +08:00
Alex Holliday 1542cf01a7 comment out pagespeed check, TODO later 2025-06-13 10:13:22 +08:00
Alex Holliday a63bcb5b4a add missing finally block 2025-06-13 10:05:29 +08:00
Alex Holliday deeb6a030c remove settings reducer 2025-06-13 10:00:48 +08:00
Alex Holliday 5e6aaa3caa remove unused reducers 2025-06-13 09:59:01 +08:00
Alex Holliday 9503f08dd8 remove uptime monitor reducer 2025-06-13 09:56:06 +08:00
Alex Holliday fca39f1914 remove redux from settings 2025-06-13 09:51:26 +08:00
Alex Holliday 8d512b4719 remove redux 2025-06-13 09:27:29 +08:00
Alex Holliday 9f204b36b0 remove unused redux state from sidebar 2025-06-13 09:18:45 +08:00
Alex Holliday a93edc1131 remove redux from action menu 2025-06-13 09:16:07 +08:00
Alexander Holliday 09b7dc8401 Merge pull request #2431 from bluewave-labs/fix/redux-removal-hook-cleanup
fix: redux removal hook cleanup
2025-06-13 09:03:25 +08:00
Alexander Holliday 6f3c2df6ee Merge pull request #2430 from bluewave-labs/feat/bg-svg
feat: add a background component
2025-06-13 09:02:33 +08:00
Alex Holliday a111903965 use hooks 2025-06-12 15:47:59 +08:00
Alex Holliday d62fe5f4ab add missing dependencies 2025-06-12 15:47:43 +08:00
Alex Holliday 289aa4fe32 refactor check hooks 2025-06-12 15:47:21 +08:00
Alex Holliday cf84d1fde5 use arrow functions to preserve context 2025-06-12 15:46:48 +08:00
Alex Holliday 50986d4493 use hooks 2025-06-12 12:44:46 +08:00
Alex Holliday 948e5e932b refactor hooks to common util 2025-06-12 12:44:35 +08:00
Alex Holliday 1574624f69 add a background component 2025-06-12 09:37:51 +08:00
Alexander Holliday 275f2384c0 Merge pull request #2426 from bluewave-labs/feat/register
feat: register
2025-06-12 09:05:21 +08:00
Alex Holliday b58fe4ae74 add loading state 2025-06-12 09:04:19 +08:00
Alexander Holliday b1f4c64a21 Merge pull request #2425 from bluewave-labs/feat/login
feat: login
2025-06-12 09:01:58 +08:00
Alexander Holliday 4e10325eea Merge pull request #2429 from anuragparashar26/develop
fix: update TextInput border color to match dropdown (#2406)
2025-06-12 09:01:40 +08:00
anuragparashar26 6dea8b434b fix: update TextInput border color to match dropdown (#2406) 2025-06-12 05:20:16 +05:30
anuragparashar26 9242612f30 fix: update TextInput border color to match dropdown (#2406) 2025-06-12 05:01:09 +05:30
Br0wnHammer de3d38d891 Implement remaining email config 2025-06-12 00:39:47 +05:30
Alex Holliday 0e4b371925 add login header 2025-06-11 20:12:02 +08:00
Br0wnHammer 1794429cbe Formatting Run 2025-06-11 15:29:25 +05:30
Br0wnHammer 7e1a7066fc TLS Options Set Up 2025-06-11 15:11:28 +05:30
Br0wnHammer 01ed3d8aaa Feat: New Email Settings 2025-06-11 14:42:51 +05:30
Br0wnHammer ec25ebee35 Fix: Node Mailer Settings 2025-06-11 14:11:49 +05:30
Alex Holliday c0ad2dfce2 fix superadmin state 2025-06-11 12:41:58 +08:00
Alex Holliday bcea3a5e66 refactor register page 2025-06-11 12:32:36 +08:00
Alex Holliday 3ade8b5bd4 update admin check HOC 2025-06-11 12:32:16 +08:00
Alex Holliday fac960b9ac set minHeight, force lowercase for email 2025-06-11 12:32:03 +08:00
Alex Holliday 045b506e2a remove unused component 2025-06-11 10:22:17 +08:00
Alex Holliday 9e6b826f6e update translations 2025-06-11 10:21:47 +08:00
Alex Holliday cd17ecdadb fix import path 2025-06-11 10:21:33 +08:00
Alex Holliday f32892adaf add components directory 2025-06-11 10:20:36 +08:00
Alex Holliday 18ef81f6f1 refactor login page 2025-06-11 10:20:14 +08:00
Alexander Holliday 3cd8191aa4 Merge pull request #2422 from bluewave-labs/feat/notification-on-click
feat: add row click handling to notificaitons table
2025-06-10 14:21:29 +08:00
Alexander Holliday ff0d338899 Merge pull request #2423 from bluewave-labs/hotfix/docker-image-name
hotfix: image name
2025-06-10 14:20:52 +08:00
Alex Holliday e44114b5e5 fix image name 2025-06-10 14:19:57 +08:00
Alex Holliday 7587fea6d5 Add row click handling to notificaitons table 2025-06-10 14:03:49 +08:00
Alexander Holliday a105e85a5a Merge pull request #2421 from bluewave-labs/hotfix/duplicate-strings
hotfix: duplicate strings, remove uptime percentage
2025-06-10 13:58:44 +08:00
Alex Holliday 3224feeb9e fix duplicate strings, remove uptime percentage 2025-06-10 13:57:00 +08:00
Alexander Holliday cb98359d35 Merge pull request #2420 from bluewave-labs/feat/test-notification
feat: test notification
2025-06-10 13:49:16 +08:00
Alexander Holliday 64390bae2b Merge pull request #2419 from bluewave-labs/feat/edit-notification-channel
feat: implement editing notification channels
2025-06-10 13:47:25 +08:00
Alexander Holliday 94d10b6385 Merge pull request #2418 from bluewave-labs/fix/delete-cascade
fix: implement delete cascade
2025-06-10 13:45:55 +08:00
Alex Holliday 08875b07ec add pagerduty 2025-06-10 13:44:15 +08:00
Alex Holliday c20b8c665a add tests notification for email 2025-06-10 13:20:44 +08:00
Alex Holliday e6493df881 add test notificaiton button and methods 2025-06-10 12:07:31 +08:00
Alex Holliday 1ed317d0cb update fallback 2025-06-10 11:29:03 +08:00
Alex Holliday df63e74116 update icon 2025-06-10 11:22:24 +08:00
Alex Holliday 910bf4c72e formatting 2025-06-10 11:16:39 +08:00
Alex Holliday 0ac1f13043 implement editing notification channels 2025-06-10 11:13:19 +08:00
Alex Holliday 76de8f11f2 formatting 2025-06-10 09:53:17 +08:00
Alex Holliday 711e85cb6e implement delete cascade 2025-06-10 09:49:53 +08:00
Alexander Holliday f41709bd12 Merge pull request #2414 from bluewave-labs/feat/fe/uptime-notification-channels
feat: fe/uptime notification channels, resolves #2344
2025-06-10 09:24:56 +08:00
Alexander Holliday 59955cd29a Merge pull request #2413 from bluewave-labs/feat/notification-channels
feat: notification channels
2025-06-10 09:24:37 +08:00
Alexander Holliday e438e4572f Merge pull request #2417 from bluewave-labs/fix/account-delete
fix: add empty query object to fix default destructuring, resolves #2386
2025-06-10 09:17:50 +08:00
Alex Holliday f445c2b310 add empty query object to fix default destructuring 2025-06-10 09:16:38 +08:00
Alexander Holliday 5dd52808b3 Merge pull request #2416 from kelvinyim0705/develop
fix: standardize pagination alignment in ChartBox components
2025-06-10 09:00:49 +08:00
Alexander Holliday a85d2c8559 Merge pull request #2409 from bluewave-labs/Capture-version-on-details-page
Capture version on details page
2025-06-10 08:59:26 +08:00
Alexander Holliday 427f52b5a4 Merge pull request #2415 from SaikatDeb12/fix/login-error-display-message
fix: changed login error display message
2025-06-10 08:58:31 +08:00
Alexander Holliday c1b828a58e Merge pull request #2410 from ceskyDJ/i18n-refactor
i18n: Refactoring, fixes and cleaning up (part I)
2025-06-10 08:54:57 +08:00
Michal Šmahel 89bc2a9293 style: Reformat code 2025-06-09 21:54:48 +02:00
Michal Šmahel 8c9084cc7a i18n: Clean up auth-related keys
Removes only keys that aren't used on any place (I searched for
each of them in all Components and Pages).
2025-06-09 21:54:48 +02:00
Michal Šmahel 00f3941ca6 i18n: Refactor, fix and clean up auth forms 2025-06-09 21:54:47 +02:00
Michal Šmahel 74915f6840 fix: Strict password validation in login form
Strict password validation is for registration, change password,
reset password and similar forms. In login form, we don't want
to interrupt user with unnecessary validation as it's completely
irrelevant what they actually entered. The one thing it's about
is the correctness of the email and the password.

Disabling strict validation for password is a kind of security
through obscurity, which could filter some untargeted attempts
for trying passwords by bots. If the bot immediatelly receives
notes about the requirements, it has more imformation for better
attacks.
2025-06-09 21:54:47 +02:00
Michal Šmahel 3ac0eb2128 refactor: Simplify client-side email validation
We don't need to implement custom check for lowercase, as it's
been already implemented in joi library. Also we don't need to
convert email to lowercase twice (via onChange and onInput event
handlers), onChange is completely sufficient and as it's just
before form validation, it's the better one.
2025-06-09 21:54:47 +02:00
Michal Šmahel c5efa71875 i18n: Refactor and fix network error 2025-06-09 21:54:45 +02:00
Michal Šmahel 6460fe33d6 i18n: Refactor page for server unreachable and remove unused 2025-06-09 21:53:04 +02:00
Michal Šmahel 6b81eabf07 i18n: Fix theme toggle 2025-06-09 21:53:03 +02:00
Michal Šmahel d63e3db216 i18n: Refactor and fix app names (Checkmate and Capture) 2025-06-09 21:53:02 +02:00
Michal Šmahel 7e873161bc l10n(ar): Remove empty localization 2025-06-09 21:53:00 +02:00
Michal Šmahel fe5f24ef44 l10n(cs): Clean up localization file 2025-06-09 21:52:53 +02:00
Kelvin Yim 21a26a5ebb fix: standardize pagination alignment in ChartBox components
Remove alignItems="center" from ChartBox to prevent unintended centering of pagination controls in uptime details, ensuring consistent pagination layout across all tables per

issue #2405
2025-06-09 17:36:26 +01:00
Owaise Imdad 4608390cfd Added local support for the page. 2025-06-09 21:25:34 +05:30
SaikatDeb12 a73d3ec83e fix: changed login error display message 2025-06-09 16:03:44 +05:30
Alex Holliday 1b40aa0ded add notifications to infra page, add generic webhook notification channel 2025-06-09 14:59:26 +08:00
Alex Holliday de673ea5d1 add notifications to pagespeed 2025-06-09 14:22:11 +08:00
Alex Holliday 4c3ea2fd65 add validation 2025-06-09 12:59:02 +08:00
Alex Holliday f2000ffe56 add notificaitons to projection 2025-06-09 12:58:52 +08:00
Alex Holliday a0de1e3c72 remove old notificaiton code 2025-06-09 12:58:25 +08:00
Alex Holliday b825e0fef4 remove old notification code 2025-06-09 12:58:04 +08:00
Alex Holliday 5b21906894 add notificaion pages 2025-06-09 12:57:30 +08:00
Alex Holliday 46b2b4c3ab add hooks 2025-06-09 12:57:15 +08:00
Alex Holliday ff11a91190 add notification config component 2025-06-09 12:57:02 +08:00
Alex Holliday eb79eef2a2 Add network service methods 2025-06-09 12:56:45 +08:00
Alex Holliday e8a4496199 add notification channel pages 2025-06-09 12:56:24 +08:00
Alex Holliday 04ab4e23db add notificaions channel to Uptime Configure 2025-06-09 12:56:05 +08:00
Alex Holliday e36abfa9a6 add notificaitons channel to Create Uptime 2025-06-09 12:55:45 +08:00
Alex Holliday 358381ddc2 update client side validation 2025-06-09 12:55:13 +08:00
Alex Holliday 1fbfbc4204 add strings 2025-06-09 12:55:01 +08:00
Alex Holliday 95703c1505 remove console.log 2025-06-09 12:54:41 +08:00
Alex Holliday 0225b93c72 add notifications channel to sidebar 2025-06-09 12:52:52 +08:00
Alex Holliday 3027606524 remove console.log 2025-06-09 12:52:32 +08:00
Alex Holliday 92ddf023ec add pager duty handling method 2025-06-09 10:49:52 +08:00
Alex Holliday 5a50e9b301 Add controller, db, and validation for notifications 2025-06-09 10:30:21 +08:00
Alex Holliday 0f10a39f91 add notification routes 2025-06-09 10:25:06 +08:00
Alex Holliday 55a871cb3c update notification model 2025-06-09 10:24:06 +08:00
Alexander Holliday bd78dbb95d Merge pull request #2412 from bluewave-labs/feat/purge-distributed-uptime
fix: purge distributed uptime
2025-06-09 10:19:36 +08:00
Alex Holliday 274595a533 purge distributed uptime 2025-06-09 10:17:09 +08:00
Alexander Holliday 5690d91d59 Merge pull request #2408 from varunrmantri23/develop
fix: prevent monitor URL and name overflow with sx Prop
2025-06-09 09:53:06 +08:00
Alexander Holliday 1cdb3d1ef7 Merge pull request #2411 from Shivansh-22866/fix/remove_future_features
Fix/remove future features
2025-06-09 09:39:46 +08:00
Shivansh-22866 048c0a7994 Merge branch 'fix/remove_future_features' of https://github.com/Shivansh-22866/Checkmate into fix/remove_future_features 2025-06-08 23:21:14 +05:30
Shivansh-22866 8412e0f17f Add comments 2025-06-08 23:20:48 +05:30
Shivansh e04bfcc96a Merge branch 'bluewave-labs:develop' into fix/remove_future_features 2025-06-08 23:18:12 +05:30
Shivansh-22866 f6f1842863 Merge branch 'develop' of https://github.com/Shivansh-22866/Checkmate into fix/remove_future_features 2025-06-08 23:16:40 +05:30
varunrmantri23 5e2fa7a8f0 fix: add global Typography variants for monitor text truncation 2025-06-08 23:12:48 +05:30
Shivansh-22866 d0dbc75587 Remove future email features from Page Speed configure 2025-06-08 19:27:33 +05:30
Owaise Imdad 3886617b11 Fix formatted the files. 2025-06-08 17:32:45 +05:30
varunrmantri23 e4ade39db7 fix: prevent monitor URL and name overflow using MUI sx prop 2025-06-08 14:33:56 +05:30
varunrmantri23 1f6826e045 fix: add CSS classes to prevent URL and name overflow in monitors 2025-06-08 13:11:04 +05:30
Gorkem Cetin (BWL) 6d236477ac Add npm run format directive 2025-06-08 00:30:59 -04:00
Alexander Holliday 32817829d8 Merge pull request #2404 from bluewave-labs/feat/pulse
feat: pulse
2025-06-08 09:03:34 +08:00
Alex Holliday afbd1157c0 add factory method 2025-06-08 09:01:30 +08:00
Alex Holliday 8de2c0387c fix method name 2025-06-08 08:57:45 +08:00
Alex Holliday 267693668d format fix 2025-06-08 08:42:16 +08:00
Alex Holliday 7475dcfb21 add update job method 2025-06-08 08:41:42 +08:00
Alexander Holliday 6d43665e59 Merge pull request #2400 from bluewave-labs/fix/general-settings-desc
Minor changes/typo fix in general settings description
2025-06-08 08:19:44 +08:00
Alexander Holliday a98c36aea3 Merge pull request #2403 from bluewave-labs/fix/port-monitor-status-page
Fix #2365: Add port monitor support to status page
2025-06-08 08:19:14 +08:00
Alexander Holliday fdf8e88e44 Merge pull request #2398 from bluewave-labs/Fix-for-csv-lowecase-check
FIX for csv file case sensitive file ending.
2025-06-08 08:18:12 +08:00
Alex Holliday f2b7a9cc67 fix formatting 2025-06-08 08:14:17 +08:00
Alex Holliday ecd8ca0940 fix formatting 2025-06-08 08:11:31 +08:00
Alex Holliday dfe88b30e8 fix formatting 2025-06-08 08:09:00 +08:00
mohadeseh safari a13056bd1b Fix #2365: Add port monitor support to status page 2025-06-07 19:12:22 -04:00
Owaise Imdad ccc9faa892 Feature for adding capture version on the deatils page. 2025-06-08 00:13:47 +05:30
Vishnu Sreekumaran Nair fe041bc0c0 fix typo in docker desc 2025-06-07 14:20:26 -04:00
Vishnu Sreekumaran Nair 1bf1387a05 update ping monitoring desc 2025-06-07 14:19:43 -04:00
Owaise Imdad 958a8ad5a7 FIX for csv file case sensitive file ending. 2025-06-07 21:14:03 +05:30
Alexander Holliday 9c921896fd Merge pull request #2331 from Thirukumaran-T/save-button-enhancement
REFACTOR: added sticky behaviour to the save button at the end
2025-06-07 20:48:33 +08:00
Alexander Holliday b0cf59b0f9 Merge pull request #2396 from bluewave-labs/translation-update-71
🌐 Updated Translations from POEditor
2025-06-07 08:24:10 +08:00
GitHub Action ded137a197 feat: translations updated from POEditor 2025-06-07 00:23:08 +00:00
Alex Holliday 9a22d4a971 initial Pulse implementation 2025-06-06 14:05:59 +08:00
Alex Holliday 9e42fd6ffd change workflow names 2025-06-06 07:44:19 +08:00
Thirukumaran-T a101bd479a CHORE: added border styles to the save button container 2025-06-05 12:00:16 +05:30
Alexander Holliday a597d23c95 Merge pull request #2394 from bluewave-labs/feat/image-tags
version tagging images
2025-06-04 19:51:06 -07:00
Alex Holliday 9591ba3dc4 version tagging images 2025-06-05 10:44:27 +08:00
Gorkem Cetin (BWL) 1f9fbf7115 Update PULLREQUESTS.md 2025-06-04 09:09:54 -04:00
Gorkem Cetin (BWL) 1286dd30b3 Update README.md 2025-06-02 22:43:56 -04:00
Alexander Holliday 8498d7abc9 Merge pull request #2381 from bluewave-labs/general-settings-description
Change general settings description according to selected uptime monitor type
2025-06-01 12:05:25 -07:00
Alexander Holliday 41e3e262fb Merge pull request #2382 from bluewave-labs/feat/format-check
Feat/format check
2025-06-01 11:57:08 -07:00
Alex Holliday c119df1553 format 2025-06-01 11:55:16 -07:00
Alex Holliday c02ba84864 user pr number 2025-06-01 11:51:49 -07:00
Alex Holliday fabbc659ad checkout repo 2025-06-01 11:49:33 -07:00
Alex Holliday 4241758354 use github cli 2025-06-01 11:47:13 -07:00
Alex Holliday 59f1ce7d31 add dependencies 2025-06-01 11:42:04 -07:00
Alex Holliday 2e7b67d991 auto-close 2025-06-01 11:39:17 -07:00
Alex Holliday 62a79aaf4b fix formatting 2025-06-01 11:31:18 -07:00
Alex Holliday 1b97a2d488 add formatting check action 2025-06-01 11:28:15 -07:00
Vishnu Sreekumaran Nair 4e414e289d update translation values for ru 2025-06-01 14:23:00 -04:00
Alexander Holliday 4ed4903272 Merge pull request #2379 from bluewave-labs/fix/2336-settings-select-value-type
Resolve Settings page Select component errors
2025-06-01 11:16:31 -07:00
Alexander Holliday 8075d3c63c Merge branch 'develop' into fix/2336-settings-select-value-type 2025-06-01 11:16:20 -07:00
Vishnu Sreekumaran Nair d6658d16f9 update translation values for pt-BR 2025-06-01 14:14:35 -04:00
Vishnu Sreekumaran Nair 6b539b009b update the en translation values 2025-06-01 14:03:56 -04:00
Vishnu Sreekumaran Nair 50e6767259 dynamically update string based on selected monitor.type 2025-06-01 13:56:13 -04:00
Vishnu Sreekumaran Nair bc06db03a2 group general instructions and update translation files 2025-06-01 13:55:19 -04:00
Alexander Holliday b8284bfddf Merge pull request #2376 from bluewave-labs/develop
Develop -> Master
2025-05-30 15:11:37 -07:00
Alexander Holliday c84322c142 Merge pull request #2375 from bluewave-labs/fix/i18n
fix: refactor i18n, resolves #2374
2025-05-30 15:11:05 -07:00
Alex Holliday 70055c712b refactor i18n 2025-05-30 15:03:32 -07:00
Alexander Holliday fd0b1633de Merge pull request #2361 from bluewave-labs/fix/2343-timezone-save-button
# Fix: Save button disabled when timezone changes (#2343)
2025-05-30 14:05:37 -07:00
mohadeseh safari aa91c9ff86 Resolve Settings page Select component errors
- Enhanced Select component to accept boolean values in PropTypes
- Updated component documentation to reflect new type support
- Added default value for showURL prop to prevent undefined value errors

- Fixed two related issues:
  1. Type mismatch error when passing boolean values
  2. Required prop error during initial rendering
2025-05-29 21:01:09 -04:00
Alexander Holliday 2486bfa146 Merge pull request #2372 from bluewave-labs/develop
Develop -> Master
2025-05-29 17:11:29 -07:00
Alexander Holliday 4f678827f3 Merge pull request #2370 from bluewave-labs/fix/webhook-notifications
fix: add config for webhook
2025-05-29 17:10:44 -07:00
Alex Holliday 5018a2dfde add config for webhook 2025-05-29 17:09:47 -07:00
mohadeseh safari 126fda6bc0 remove call 2025-05-29 19:34:21 -04:00
Alexander Holliday a3fb77565a Merge pull request #2367 from bluewave-labs/develop
Develop -> Master
2025-05-28 16:52:48 -07:00
Alexander Holliday 064fb1bfd9 Merge pull request #2360 from bluewave-labs/fix/2286-maintenance-placeholder-flash
Fix Maintenance Placeholder Flash (#2286)
2025-05-28 10:20:32 -07:00
mohadeseh safari ed5d5986ff Fix #2343: Enable Save button when timezone changes
- Added timezone field to frontend validation schema
- Added timezone field to backend validation schema
- Fixed state synchronization in Settings component
2025-05-27 20:28:53 -04:00
mohadeseh safari e6c2e34b78 fix(maintenance): prevent placeholder flash when maintenance items exist (#2286) 2025-05-27 19:54:31 -04:00
Thirukumaran-T 9ce1bf9bfb CHORE: reduced padding to make the whole stack thinner 2025-05-27 10:15:05 +05:30
Thirukumaran-T 76d69005aa REFACTOR: added sticky behaviour to the save button at the end 2025-05-24 18:33:05 +05:30
301 changed files with 14389 additions and 14584 deletions

No files matched your search

+2 -1
View File
@@ -20,7 +20,8 @@ const { t } = useTranslation();
```
- [ ] I have **not** included any files that are not related to my pull request, including package-lock and package-json if dependencies have not changed
- [ ] 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. I have no hardcoded dimensions.
- [ ] I made sure font sizes, color choices etc are all referenced from the theme. I don't have any hardcoded dimensions.
- [ ] My PR is granular and targeted to one specific feature.
- [ ] I ran `npm run format` in server and client directories, which automatically formats your code.
- [ ] I took a screenshot or a video and attached to this PR if there is a UI change.
+62
View File
@@ -0,0 +1,62 @@
name: Format Check (Client & Server)
on:
pull_request:
workflow_dispatch:
jobs:
format-client:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install client dependencies
working-directory: client
run: npm ci
- name: Check client formatting
working-directory: client
run: npm run format-check
format-server:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install server dependencies
working-directory: server
run: npm ci
- name: Check server formatting
working-directory: server
run: npm run format-check
close-pr-if-needed:
if: always()
runs-on: ubuntu-latest
needs: [format-client, format-server]
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Get PR number
id: pr
run: echo "PR_NUMBER=$(jq -r .pull_request.number "$GITHUB_EVENT_PATH")" >> $GITHUB_ENV
- name: Close PR using GitHub CLI
if: |
needs.format-client.result == 'failure' ||
needs.format-server.result == 'failure'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr close "$PR_NUMBER" --delete-branch --comment "❌ Formatting check failed — PR auto-closed.
Please run \`npm run format\` in both `client` and `server` directories and push again."
@@ -0,0 +1,159 @@
name: Deploy images on release
on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
docker-build-and-push-client:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Extract version from tag
id: extract_tag
run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Client Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-client:${{ steps.extract_tag.outputs.version }} \
-f ./docker/dist/client.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push Client Docker image
run: |
docker push ghcr.io/bluewave-labs/checkmate-client:${{ steps.extract_tag.outputs.version }}
docker-build-and-push-server:
needs: docker-build-and-push-client
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Extract version
id: extract_tag
run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Server Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-backend:${{ steps.extract_tag.outputs.version }} \
-f ./docker/dist/server.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push Server Docker image
run: |
docker push ghcr.io/bluewave-labs/checkmate-backend:${{ steps.extract_tag.outputs.version }}
- name: Build Mongo Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-mongo:${{ steps.extract_tag.outputs.version }} \
-f ./docker/dist/mongoDB.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push MongoDB Docker image
run: |
docker push ghcr.io/bluewave-labs/checkmate-mongo:${{ steps.extract_tag.outputs.version }}
- name: Build Redis Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-redis:${{ steps.extract_tag.outputs.version }} \
-f ./docker/dist/redis.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push Redis Docker image
run: |
docker push ghcr.io/bluewave-labs/checkmate-redis:${{ steps.extract_tag.outputs.version }}
docker-build-and-push-server-mono-multiarch:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract version
id: extract_tag
run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push multi-arch Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./docker/dist-arm/server.Dockerfile
push: true
tags: |
ghcr.io/bluewave-labs/checkmate-backend-mono-multiarch:${{ steps.extract_tag.outputs.version }}
platforms: linux/amd64,linux/arm64
labels: |
org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate
build-args: |
VITE_APP_VERSION=${{ steps.extract_tag.outputs.version }}
docker-build-and-push-server-mono:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Extract version
id: extract_tag
run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Server Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-backend-mono:${{ steps.extract_tag.outputs.version }} \
-f ./docker/dist-mono/server.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
--build-arg VITE_APP_VERSION=${{ steps.extract_tag.outputs.version }} \
.
- name: Push Server Docker image
run: docker push ghcr.io/bluewave-labs/checkmate-backend-mono:${{ steps.extract_tag.outputs.version }}
+154
View File
@@ -0,0 +1,154 @@
name: Deploy images
on:
push:
branches: ["master"]
workflow_dispatch:
jobs:
docker-build-and-push-client:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get version
id: vars
run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Build Client Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-client:latest \
-f ./docker/dist/client.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
--build-arg VITE_APP_VERSION=${{ env.VERSION }} \
.
- name: Push Client Docker image
run: |
docker push ghcr.io/bluewave-labs/checkmate-client:latest
docker-build-and-push-server:
needs: docker-build-and-push-client
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Server Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-backend:latest \
-f ./docker/dist/server.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push Server Docker image
run: |
docker push ghcr.io/bluewave-labs/checkmate-backend:latest
- name: Build Mongo Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-mongo:latest \
-f ./docker/dist/mongoDB.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push MongoDB Docker image
run: |
docker push ghcr.io/bluewave-labs/checkmate-mongo:latest
- name: Build Redis Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-redis:latest \
-f ./docker/dist/redis.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push Redis Docker image
run: |
docker push ghcr.io/bluewave-labs/checkmate-redis:latest
docker-build-and-push-server-mono-multiarch:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get version
id: vars
run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Build and push multi-arch Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./docker/dist-arm/server.Dockerfile
push: true
tags: |
ghcr.io/bluewave-labs/checkmate-backend-mono-multiarch:latest
platforms: linux/amd64,linux/arm64
labels: |
org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate
build-args: |
VITE_APP_VERSION=${{ env.VERSION }}
docker-build-and-push-server-mono:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get version
id: vars
run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Build Server Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate-backend-mono:latest \
-f ./docker/dist-mono/server.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
--build-arg VITE_APP_VERSION=${{ env.VERSION }} \
.
- name: Push Server Docker image
run: docker push ghcr.io/bluewave-labs/checkmate-backend-mono:latest
-80
View File
@@ -1,80 +0,0 @@
name: Distribution deploy
on:
push:
branches: ["master"]
workflow_dispatch:
jobs:
docker-build-and-push-client:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Client Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:frontend-dist \
-f ./docker/dist/client.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push Client Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:frontend-dist
docker-build-and-push-server:
needs: docker-build-and-push-client
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Server Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:backend-dist \
-f ./docker/dist/server.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push Server Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:backend-dist
- name: Build Mongo Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:mongo-dist \
-f ./docker/dist/mongoDB.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push MongoDB Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:mongo-dist
- name: Build Redis Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:redis-dist \
-f ./docker/dist/redis.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push Redis Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:redis-dist
@@ -1,44 +0,0 @@
name: Distribution deploy - Monolithic Multiarch
on:
push:
branches: ["master"]
workflow_dispatch:
jobs:
docker-build-and-push-server:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push multi-arch Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./docker/dist-arm/server.Dockerfile
push: true
tags: ghcr.io/bluewave-labs/checkmate:backend-dist-mono-multiarch
platforms: linux/amd64,linux/arm64
labels: |
org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate
# - name: Build Server Docker image
# run: |
# docker build \
# -t ghcr.io/bluewave-labs/checkmate:backend-dist-mono-multiarch \
# -f ./docker/dist-arm/server.Dockerfile \
# --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
# .
# - name: Push Server Docker image
# run: docker push ghcr.io/bluewave-labs/checkmate:backend-dist-mono-multiarch
@@ -1,30 +0,0 @@
name: Distribution deploy - Monolithic
on:
push:
branches: ["master"]
workflow_dispatch:
jobs:
docker-build-and-push-server:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Server Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:backend-dist-mono \
-f ./docker/dist-mono/server.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push Server Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:backend-dist-mono
+6
View File
@@ -10,6 +10,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
@@ -18,12 +20,16 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get version
id: vars
run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Build Client Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:frontend-demo \
-f ./docker/prod/client.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
--build-arg VITE_APP_VERSION=${{ env.VERSION }} \
.
- name: Push Client Docker image
+6
View File
@@ -10,6 +10,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
@@ -18,12 +20,16 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get version
id: vars
run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Build Client Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:frontend-staging \
-f ./docker/staging/client.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
--build-arg VITE_APP_VERSION=${{ env.VERSION }} \
.
- name: Push Client Docker image
+5 -1
View File
@@ -27,6 +27,10 @@ Process. If more revisions are required after the second review we’re looking
If PRs are small and manageable it is far more likely that a dev will catch bugs during the review process. If our eyes glaze over at line 400 of a 700 line PR since we’ve reached our cognitive limit we’re not going to likely miss bugs in the last 300 lines of code.
### Bonus Topic: Keep PRs focused
### Format your PRs for better readability
Ensure you execute `npm run format` before submitting your pull requests in both the client and server directories. This command automatically applies our formatting structure, making the code easier to follow and review.
### Keep PRs focused
It may be tempting to address a bug you suddenly remembered or make some tiny adjustments in some component that bothers you, but don’t! Keep all commits in your pull request fully focused on the specific feature you are working on. Open up another PR if you want to fix a big or work on another feature.
+31 -8
View File
@@ -14,7 +14,7 @@
<p align="center"><strong>An open source uptime and infrastructure monitoring application</strong></p>
![dashboard](https://github.com/user-attachments/assets/252d6047-522b-4576-8f14-233510e464b8)
<img width="1660" alt="image" src="https://github.com/user-attachments/assets/b748f36d-a271-4965-ad0a-18bf153bbee7" />
This repository contains both the frontend and the backend of Checkmate, an open-source, self-hosted monitoring tool for tracking server hardware, uptime, response times, and incidents in real-time with beautiful visualizations. Checkmate 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.
@@ -22,7 +22,23 @@ Checkmate also has an agent, called [Capture](https://github.com/bluewave-labs/c
Checkmate has been stress-tested with 1000+ active monitors without any particular issues or performance bottlenecks.
We **love** what we are building here, and we continuously learn a few things about Reactjs, Nodejs, MongoDB, and Docker while building Checkmate.
**If you would like to sponsor a feature, [see this link](https://checkmate.so/sponsored-features).**
## 📚 Table of contents
- [📦 Demo](#-demo)
- [🔗 User's guide](#-users-guide)
- [🛠️ Installation](#️-installation)
- [🏁 Translations](#-translations)
- [🚀 Performance](#-performance)
- [💚 Questions & Ideas](#-questions--ideas)
- [🧩 Features](#-features)
- [🏗️ Screenshots](#-screenshots)
- [🏗️ Tech stack](#-tech-stack)
- [🔗 A few links](#a-few-links)
- [🤝 Contributing](#-contributing)
- [💰 Our sponsors](#-our-sponsors)
## 📦 Demo
@@ -34,7 +50,7 @@ Usage instructions can be found [here](https://docs.checkmate.so/checkmate-2.1).
## 🛠️ Installation
See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart). Alternatively, you can also use [Coolify](https://coolify.io/) or [Elestio](https://elest.io/open-source/checkmate) 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.
See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart). Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. 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.
## 🏁 Translations
@@ -80,14 +96,19 @@ If you have any questions, suggestions or comments, please use our [Discord chan
## 🏗️ Screenshots
<p>
<img width="2714" alt="server" src="https://github.com/user-attachments/assets/f7cb272a-69a6-48c5-93b0-249ecf20ecc6" />
<img width="1628" alt="image" src="https://github.com/user-attachments/assets/2eff6464-0738-4a32-9312-26e1e8e86275" />
</p>
<p>
<img width="2714" alt="uptime" src="https://github.com/user-attachments/assets/98ddc6c0-3384-47fd-96ce-7e53e6b688ac" />
<img width="1656" alt="image" src="https://github.com/user-attachments/assets/616c3563-c2a7-4ee4-af6c-7e6068955d1a" />
</p>
<p>
<img width="2714" alt="page speed" src="https://github.com/user-attachments/assets/b5589f79-da30-4239-9846-1f8bb2637ff9" />
</p><img width="1652" alt="image" src="https://github.com/user-attachments/assets/7912d7cf-0d0e-4f26-aa5c-2ad7170b5c99" />
</p>
<p>
<img width="1652" alt="image" src="https://github.com/user-attachments/assets/08c2c6ac-3a2f-44d1-a229-d1746a3f9d16" />
</p>
## 🏗️ Tech stack
@@ -109,7 +130,7 @@ If you have any questions, suggestions or comments, please use our [Discord chan
We are [Alex](http://github.com/ajhollid) (team lead), [Vishnu](http://github.com/vishnusn77), [Mohadeseh](http://github.com/mohicody), [Gorkem](http://github.com/gorkem-bwl/), [Owaise](http://github.com/Owaiseimdad), [Aryaman](https://github.com/Br0wnHammer) and [Mert](https://github.com/mertssmnoglu) helping individuals and businesses monitor their infra and servers.
We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 5800+ stars and attracted 70+ contributors from around the globe.
We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 6000+ stars and attracted 80+ contributors from around the globe.
Our repo is starred by employees from **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes and NEC**, so don’t hold back — jump in, contribute and learn with us!
@@ -127,11 +148,13 @@ Here's how you can contribute:
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
</a>
[![Star History Chart](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
## 💰 Our sponsors
Thanks to [Gitbook](https://gitbook.io/) for giving us a free tier for their documentation platform, and [Poeditor](https://poeditor.com/) providing us a free account to use their i18n services. If you would like to sponsor Checkmate, please send an email to hello@bluewavelabs.ca
[![Star History Chart](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
If you would like to sponsor a feature, [see this page](https://checkmate.so/sponsored-features).
Also check other developer and contributor-friendly projects of BlueWave:
+4
View File
@@ -16,4 +16,8 @@ module.exports = {
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
"react/no-unescaped-entities": "off",
},
globals: {
__APP_VERSION__: "readonly",
process: "readonly",
},
};
+246 -668
View File
File diff suppressed because it is too large. Load diff
+1 -8
View File
@@ -15,13 +15,11 @@
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@fontsource/roboto": "^5.0.13",
"@hello-pangea/dnd": "^18.0.0",
"@mui/icons-material": "6.4.11",
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
"@mui/material": "6.4.11",
"@mui/x-charts": "^7.5.1",
"@mui/x-data-grid": "7.29.0",
"@mui/x-date-pickers": "7.29.0",
"@reduxjs/toolkit": "2.7.0",
"axios": "^1.7.4",
@@ -29,14 +27,9 @@
"flag-icons": "7.3.2",
"html2canvas": "^1.4.1",
"i18next": "^24.2.2",
"immutability-helper": "^3.1.1",
"joi": "17.13.3",
"jwt-decode": "^4.0.0",
"maplibre-gl": "5.3.1",
"mui-color-input": "^6.0.0",
"react": "18.3.1",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-i18next": "^15.4.0",
"react-icons": "5.5.0",
@@ -65,7 +58,7 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.6",
"prettier": "^3.3.3",
"vite": "^5.4.19"
"vite": "6.3.5"
},
"optionalDependencies": {
"@rollup/rollup-linux-arm64-musl": "4.41.0"
+5 -11
View File
@@ -10,6 +10,7 @@ import { logger } from "./Utils/Logger"; // Import the logger
import { networkService } from "./main";
import { Routes } from "./Routes";
import WalletProvider from "./Components/WalletProvider";
import AppLayout from "./Components/Layouts/AppLayout";
function App() {
const mode = useSelector((state) => state.ui.mode);
@@ -27,17 +28,10 @@ function App() {
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
<WalletProvider>
<CssBaseline />
<GlobalStyles
styles={({ palette }) => {
return {
body: {
backgroundImage: `radial-gradient(circle, ${palette.gradient.color1}, ${palette.gradient.color2}, ${palette.gradient.color3}, ${palette.gradient.color4}, ${palette.gradient.color5})`,
color: palette.primary.contrastText,
},
};
}}
/>
<Routes />
<AppLayout>
<Routes />
</AppLayout>
<ToastContainer />
</WalletProvider>
</ThemeProvider>
+19 -29
View File
@@ -1,17 +1,18 @@
// Components
import IconButton from "@mui/material/IconButton";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import Settings from "../../assets/icons/settings-bold.svg?react";
import Dialog from "../../Components/Dialog";
// Utils
import { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import { createToast } from "../../Utils/toastUtils";
import { logger } from "../../Utils/Logger";
import { IconButton, Menu, MenuItem } from "@mui/material";
import {
deleteUptimeMonitor,
pauseUptimeMonitor,
} from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
import Settings from "../../assets/icons/settings-bold.svg?react";
import PropTypes from "prop-types";
import Dialog from "../../Components/Dialog";
import { usePauseMonitor, useDeleteMonitor } from "../../Hooks/monitorHooks";
const ActionsMenu = ({
monitor,
@@ -23,38 +24,27 @@ const ActionsMenu = ({
const [anchorEl, setAnchorEl] = useState(null);
const [actions, setActions] = useState({});
const [isOpen, setIsOpen] = useState(false);
const dispatch = useDispatch();
const theme = useTheme();
const { isLoading } = useSelector((state) => state.uptimeMonitors);
const [pauseMonitor, isPausing, error] = usePauseMonitor();
const [deleteMonitor, isDeleting] = useDeleteMonitor();
const handleRemove = async (event) => {
event.preventDefault();
event.stopPropagation();
let monitor = { _id: actions.id };
const action = await dispatch(deleteUptimeMonitor({ monitor }));
if (action.meta.requestStatus === "fulfilled") {
setIsOpen(false); // close modal
updateRowCallback();
createToast({ body: "Monitor deleted successfully." });
} else {
createToast({ body: "Failed to delete monitor." });
}
await deleteMonitor({ monitor });
updateRowCallback();
};
const handlePause = async () => {
try {
setIsLoading(true);
const action = await dispatch(pauseUptimeMonitor({ monitorId: monitor._id }));
if (pauseUptimeMonitor.fulfilled.match(action)) {
const state = action?.payload?.data.isActive === false ? "resumed" : "paused";
createToast({ body: `Monitor ${state} successfully.` });
pauseCallback();
} else {
throw new Error(action?.error?.message ?? "Failed to pause monitor.");
}
await pauseMonitor({ monitorId: monitor._id });
pauseCallback();
} catch (error) {
logger.error("Error pausing monitor:", monitor._id, error);
createToast({ body: "Failed to pause monitor." });
} finally {
setIsLoading(false);
}
};
@@ -210,7 +200,7 @@ const ActionsMenu = ({
e.stopPropagation();
handleRemove(e);
}}
isLoading={isLoading}
isLoading={isDeleting}
modelTitle="modal-delete-monitor"
modelDescription="delete-monitor-confirmation"
/>
+1 -1
View File
@@ -1,7 +1,7 @@
import PropTypes from "prop-types";
import { Box, Breadcrumbs as MUIBreadcrumbs } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router";
import { useNavigate } from "react-router-dom";
import ArrowRight from "../../assets/icons/right-arrow.svg?react";
import "./index.css";
@@ -43,7 +43,6 @@ const ChartBox = ({
>
<Stack
flex={1}
alignItems="center"
sx={{
padding: theme.spacing(8),
justifyContent,
-3
View File
@@ -47,9 +47,6 @@ const AppAppBar = () => {
const location = useLocation();
const navigate = useNavigate();
// Debugging: Log the current theme mode
console.log("Current theme mode:", mode);
const logoSrc =
mode === "light" ? "/images/prism-black.png" : "/images/prism-white.png";
+1 -1
View File
@@ -57,7 +57,7 @@ Dialog.propTypes = {
onCancel: PropTypes.func.isRequired,
confirmationButtonLabel: PropTypes.string.isRequired,
onConfirm: PropTypes.func.isRequired,
isLoading: PropTypes.bool.isRequired,
isLoading: PropTypes.bool,
};
export default Dialog;
+5 -1
View File
@@ -1,19 +1,22 @@
[class*="fallback__"] {
width: fit-content;
margin: auto;
margin-top: 100px;
}
[class*="fallback__"] h1.MuiTypography-root {
font-size: var(--env-var-font-size-large);
font-weight: 600;
}
[class*="fallback__"] button.MuiButtonBase-root,
[class*="fallback__"] .check {
width: max-content;
}
[class*="fallback__"] button.MuiButtonBase-root {
min-height: 34px;
}
[class*="fallback__"] .check span.MuiTypography-root,
[class*="fallback__"] button.MuiButtonBase-root {
font-size: var(--env-var-font-size-medium);
@@ -35,6 +38,7 @@
background-size: cover;
background-repeat: no-repeat;
}
.fallback__status > .MuiStack-root {
margin-left: var(--env-var-spacing-2);
}
+116 -90
View File
@@ -11,7 +11,7 @@ import { useSelector } from "react-redux";
import Alert from "../Alert";
import { useTranslation } from "react-i18next";
import "./index.css";
import { useFetchSettings } from "../../Hooks/useFetchSettings";
import { useFetchSettings } from "../../Hooks/settingsHooks";
import { useState } from "react";
/**
* Fallback component to display a fallback UI with a title, a list of checks, and a navigation button.
@@ -39,7 +39,11 @@ const Fallback = ({
const { t } = useTranslation();
const [settingsData, setSettingsData] = useState(undefined);
const [isLoading, error] = useFetchSettings({ setSettingsData });
const [isLoading, error] = useFetchSettings({
setSettingsData,
setIsApiKeySet: () => {},
setIsEmailPasswordSet: () => {},
});
// Custom warning message with clickable link
const renderWarningMessage = () => {
return (
@@ -63,113 +67,135 @@ const Fallback = ({
</>
);
};
return (
<Box
position="relative"
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.primary.main}
overflow="hidden"
sx={{
borderStyle: "dashed",
minHeight: "calc(100vh - var(--env-var-spacing-2) * 2)",
height: "100%",
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<Stack
className={`fallback__${title?.trim().split(" ")[0]}`}
alignItems="center"
gap={theme.spacing(20)}
<Box
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.primary.main}
overflow="hidden"
sx={{
display: "flex",
borderStyle: "dashed",
height: {
sm: "50vh",
md: "70vh",
lg: "60vh",
xl: "55vh",
},
width: {
sm: "90%",
md: "50%",
lg: "40%",
},
padding: theme.spacing(10),
}}
>
{mode === "light" ? (
<Skeleton style={{ zIndex: 1 }} />
) : (
<SkeletonDark style={{ zIndex: 1 }} />
)}
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
<Stack
gap={theme.spacing(4)}
maxWidth={"300px"}
zIndex={1}
className={`fallback__${title?.trim().split(" ")[0]}`}
alignItems="center"
gap={theme.spacing(20)}
>
<Typography
component="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
{mode === "light" ? (
<Skeleton style={{ zIndex: 1 }} />
) : (
<SkeletonDark style={{ zIndex: 1 }} />
)}
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
{vowelStart ? "An" : "A"} {title} is used to:
</Typography>
{checks?.map((check, index) => (
<Check
text={check}
key={`${title.trim().split(" ")[0]}-${index}`}
outlined={true}
/>
))}
</Stack>
{/* TODO - display a different fallback if user is not an admin*/}
{isAdmin && (
<>
<Button
variant="contained"
color="accent"
sx={{ alignSelf: "center" }}
onClick={() => navigate(link)}
<Background style={{ width: "100%" }} />
</Box>
<Stack
gap={theme.spacing(4)}
maxWidth={theme.spacing(180)}
zIndex={1}
>
<Typography
alignSelf="center"
component="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
Let's create your first {title}
</Button>
{/* Bulk create of uptime monitors */}
{title === "uptime monitor" && (
{vowelStart ? "An" : "A"} {title} is used to:
</Typography>
{checks?.map((check, index) => (
<Check
text={check}
key={`${title.trim().split(" ")[0]}-${index}`}
outlined={true}
/>
))}
</Stack>
{/* TODO - display a different fallback if user is not an admin*/}
{isAdmin && (
<>
<Button
variant="contained"
color="accent"
sx={{ alignSelf: "center" }}
onClick={() => navigate("/uptime/bulk-import")}
onClick={() => navigate(link)}
>
{t("bulkImport.fallbackPage")}
Let's create your first {title}
</Button>
)}
{/* Warning box for PageSpeed monitor */}
{title === "pagespeed monitor" && showPageSpeedWarning && (
<Box sx={{ width: "80%", maxWidth: "600px", zIndex: 1 }}>
<Box
sx={{
"& .alert.row-stack": {
backgroundColor: theme.palette.warningSecondary.main,
borderColor: theme.palette.warningSecondary.lowContrast,
"& .MuiTypography-root": {
color: theme.palette.warningSecondary.contrastText,
},
"& .MuiBox-root > svg": {
color: theme.palette.warningSecondary.contrastText,
},
},
}}
{/* Bulk create of uptime monitors */}
{title === "uptime monitor" && (
<Button
variant="contained"
color="accent"
sx={{ alignSelf: "center" }}
onClick={() => navigate("/uptime/bulk-import")}
>
{settingsData?.pagespeedKeySet === false && (
<Alert
variant="warning"
hasIcon={true}
body={renderWarningMessage()}
/>
)}
{t("bulkImport.fallbackPage")}
</Button>
)}
{/* Warning box for PageSpeed monitor */}
{title === "pagespeed monitor" && showPageSpeedWarning && (
<Box sx={{ width: "80%", maxWidth: "600px", zIndex: 1 }}>
<Box
sx={{
"& .alert.row-stack": {
backgroundColor: theme.palette.warningSecondary.main,
borderColor: theme.palette.warningSecondary.lowContrast,
"& .MuiTypography-root": {
color: theme.palette.warningSecondary.contrastText,
},
"& .MuiBox-root > svg": {
color: theme.palette.warningSecondary.contrastText,
},
},
}}
>
{settingsData?.pagespeedKeySet === false && (
<Alert
variant="warning"
hasIcon={true}
body={renderWarningMessage()}
/>
)}
</Box>
</Box>
</Box>
)}
</>
)}
</Stack>
)}
</>
)}
</Stack>
</Box>
</Box>
);
};
@@ -1,8 +1,11 @@
import { Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
const NetworkError = () => {
const theme = useTheme();
const { t } = useTranslation();
return (
<>
<Typography
@@ -10,9 +13,9 @@ const NetworkError = () => {
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
Network error
{t("common.toasts.networkError")}
</Typography>
<Typography>Please check your connection</Typography>
<Typography>{t("common.toasts.checkConnection")}</Typography>
</>
);
};
+15 -3
View File
@@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { logger } from "../../Utils/Logger";
@@ -7,23 +7,35 @@ import { networkService } from "../../main";
const withAdminCheck = (WrappedComponent) => {
const WithAdminCheck = (props) => {
const navigate = useNavigate();
const [isChecking, setIsChecking] = useState(true);
const [superAdminExists, setSuperAdminExists] = useState(false);
useEffect(() => {
networkService
.doesSuperAdminExist()
.then((response) => {
if (response.data.data === true) {
if (response?.data?.data === true) {
navigate("/login");
} else {
setSuperAdminExists(false);
}
})
.catch((error) => {
logger.error(error);
})
.finally(() => {
setIsChecking(false);
});
}, [navigate]);
if (isChecking) {
return null;
}
return (
<WrappedComponent
{...props}
isSuperAdmin={true}
superAdminExists={superAdminExists}
/>
);
};
@@ -0,0 +1,16 @@
import i18n from "../../Utils/i18n";
import { useSelector } from "react-redux";
import { useEffect } from "react";
const I18nLoader = () => {
const language = useSelector((state) => state.ui.language);
useEffect(() => {
if (language && i18n.language !== language) {
i18n.changeLanguage(language);
}
}, [language]);
return null;
};
export default I18nLoader;
@@ -105,7 +105,7 @@ const Checkbox = ({
};
Checkbox.propTypes = {
id: PropTypes.string.isRequired,
id: PropTypes.string,
name: PropTypes.string,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
size: PropTypes.oneOf(["small", "medium", "large"]),
+20 -14
View File
@@ -16,26 +16,27 @@ import "./index.css";
* size="small"
* />
*
* @param {Object} props - The component props.
* @param {string} props.id - The id of the radio button.
* @param {string} props.title - The title of the radio button.
* @param {string} [props.desc] - The description of the radio button.
* @param {string} [props.size="small"] - The size of the radio button.
* @param {Object} props - The component
* @param {string} id - The id of the radio button.
* @param {string} title - The title of the radio button.
* @param {string} [desc] - The description of the radio button.
* @param {string} [size="small"] - The size of the radio button.
* @returns {JSX.Element} - The rendered Radio component.
*/
const Radio = (props) => {
const Radio = ({ name, checked, value, id, size, title, desc, onChange }) => {
const theme = useTheme();
return (
<FormControlLabel
className="custom-radio-button"
checked={props.checked}
value={props.value}
name={name}
checked={checked}
value={value}
control={
<MUIRadio
id={props.id}
size={props.size}
id={id}
size={size}
checkedIcon={<RadioChecked />}
sx={{
color: "transparent",
@@ -49,16 +50,16 @@ const Radio = (props) => {
}}
/>
}
onChange={props.onChange}
onChange={onChange}
label={
<>
<Typography component="p">{props.title}</Typography>
<Typography component="p">{title}</Typography>
<Typography
component="h6"
mt={theme.spacing(1)}
color={theme.palette.primary.contrastTextSecondary}
>
{props.desc}
{desc}
</Typography>
</>
}
@@ -81,9 +82,14 @@ const Radio = (props) => {
};
Radio.propTypes = {
title: PropTypes.string.isRequired,
title: PropTypes.string,
desc: PropTypes.string,
size: PropTypes.string,
name: PropTypes.string,
checked: PropTypes.bool,
value: PropTypes.string,
id: PropTypes.string,
onChange: PropTypes.func,
};
export default Radio;
+87 -25
View File
@@ -1,7 +1,17 @@
import PropTypes from "prop-types";
import { Box, ListItem, Autocomplete, TextField, Stack, Typography } from "@mui/material";
import {
Box,
ListItem,
Autocomplete,
TextField,
Stack,
Typography,
Checkbox,
} from "@mui/material";
import { useTheme } from "@emotion/react";
import SearchIcon from "../../../assets/icons/search.svg?react";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
/**
* Search component using Material UI's Autocomplete.
@@ -60,24 +70,72 @@ const Search = ({
onBlur,
}) => {
const theme = useTheme();
const { t } = useTranslation();
const [selectAll, setSelectAll] = React.useState(false);
const [open, setOpen] = React.useState(false);
const enhancedOptions = React.useMemo(() => {
return multiple && isAdorned
? [
{ [filteredBy]: t("selectAll"), isSelectAll: true, _id: "select_all" },
...options,
]
: options;
}, [multiple, isAdorned, options, filteredBy]);
const isOptionSelected = (option) => {
if (!multiple && !isAdorned) return false;
if (Array.isArray(value)) {
return value.some((item) => item._id === option._id);
}
return false;
};
const handleSelectAll = (isSelectAll) => {
const newValue = isSelectAll ? [...options] : [];
handleChange(newValue);
setSelectAll(isSelectAll);
};
useEffect(() => {
const allSelected =
Array.isArray(value) && Array.isArray(options) && value.length === options.length;
if (selectAll !== allSelected) setSelectAll(allSelected);
}, [value, options]);
return (
<Autocomplete
onBlur={onBlur}
multiple={multiple}
id={id}
value={value}
open={open}
onOpen={() => setOpen(true)}
onClose={(event, reason) => {
if (reason === "blur" || reason === "escape") {
setOpen(false);
}
}}
inputValue={inputValue}
onInputChange={(_, newValue) => {
handleInputChange(newValue);
}}
onChange={(_, newValue) => {
handleChange(newValue);
if (multiple && isAdorned) {
const hasSelectAllSelected =
Array.isArray(newValue) && newValue.some((item) => item.isSelectAll);
if (hasSelectAllSelected) {
handleSelectAll(!selectAll);
} else {
handleChange(newValue);
setSelectAll(Array.isArray(newValue) && newValue.length === options.length);
}
} else {
handleChange(newValue);
setOpen(false);
}
}}
fullWidth
freeSolo
disabled={disabled}
disableClearable
options={options}
options={enhancedOptions}
getOptionLabel={(option) => option[filteredBy]}
isOptionEqualToValue={(option, value) => option._id === value._id} // Compare by unique identifier
renderInput={(params) => (
@@ -102,27 +160,7 @@ const Search = ({
...(endAdornment && { endAdornment: endAdornment }),
},
}}
sx={{
"& fieldset": {
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
},
"& .MuiOutlinedInput-root:hover:not(:has(input:focus)):not(:has(textarea:focus)) fieldset":
{
borderColor: theme.palette.primary.lowContrast,
},
"& .MuiOutlinedInput-root": {
paddingY: 0,
},
"& .MuiAutocomplete-tag": {
// CAIO_REVIEW
color: theme.palette.primary.contrastText,
backgroundColor: theme.palette.primary.lowContrast,
},
"& .MuiChip-deleteIcon": {
color: theme.palette.primary.contrastText, // CAIO_REVIEW
},
}}
sx={{}}
/>
{error && (
<Typography
@@ -140,6 +178,9 @@ const Search = ({
</Stack>
)}
filterOptions={(options, { inputValue }) => {
if (inputValue.trim() === "" && multiple && isAdorned) {
return enhancedOptions;
}
const filtered = options.filter((option) =>
option[filteredBy].toLowerCase().includes(inputValue.toLowerCase())
);
@@ -156,6 +197,7 @@ const Search = ({
const { key, ...optionProps } = props;
const hasSecondaryLabel = secondaryLabel && option[secondaryLabel] !== undefined;
const port = option["port"];
const selected = isOptionSelected(option);
return (
<ListItem
key={key}
@@ -166,9 +208,29 @@ const Search = ({
pointerEvents: "none",
backgroundColor: theme.palette.primary.main,
}
: {}
: option.isSelectAll
? {
fontWeight: "bold",
backgroundColor: theme.palette.primary.light,
"&:hover": {
backgroundColor: theme.palette.primary.light,
},
}
: {}
}
>
{multiple && isAdorned && !option.noOptions && (
<Checkbox
checked={option.isSelectAll ? selectAll : selected}
sx={{
color: theme.palette.primary.contrastTextSecondary,
"&.Mui-checked": {
color: theme.palette.secondary.main,
},
padding: 0,
}}
/>
)}
{option[filteredBy] +
(hasSecondaryLabel
? ` (${option[secondaryLabel]}${port ? `: ${port}` : ""})`
@@ -12,9 +12,9 @@ import "./index.css";
* @param {string} props.placeholder - The label of the select element.
* @param {string} props.placeholder - The placeholder text when no option is selected.
* @param {boolean} props.isHidden - Whether the placeholder should be hidden.
* @param {string} props.value - The currently selected value.
* @param {(string | number | boolean)} props.value - The currently selected value.
* @param {object[]} props.items - The array of items to populate in the select dropdown.
* @param {(string | number)} props.items._id - The unique identifier of each item.
* @param {(string | number | boolean)} props.items._id - The unique identifier of each item.
* @param {string} props.items.name - The display name of each item.
* @param {function} props.onChange - The function to handle onChange event.
* @param {object} props.sx - The custom styles object for MUI Select component.
@@ -161,10 +161,13 @@ Select.propTypes = {
label: PropTypes.string,
placeholder: PropTypes.string,
isHidden: PropTypes.bool,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool])
.isRequired,
items: PropTypes.arrayOf(
PropTypes.shape({
_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool])
.isRequired,
name: PropTypes.string.isRequired,
})
).isRequired,
+4 -3
View File
@@ -3,15 +3,17 @@ import { Box, MenuItem, Select, Stack } from "@mui/material";
import { useTheme } from "@emotion/react";
import "flag-icons/css/flag-icons.min.css";
import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import { setLanguage } from "../Features/UI/uiSlice";
const LanguageSelector = () => {
const { i18n } = useTranslation();
const theme = useTheme();
const { language } = useSelector((state) => state.ui);
const dispatch = useDispatch();
const handleChange = (event) => {
const newLang = event.target.value;
i18n.changeLanguage(newLang);
dispatch(setLanguage(newLang));
};
const languages = Object.keys(i18n.options.resources || {});
@@ -32,7 +34,6 @@ const LanguageSelector = () => {
}
if (parsedLang.includes("-")) {
parsedLang = parsedLang.split("-")[1].toLowerCase();
console.log("parsedLang", parsedLang);
}
const flag = parsedLang ? `fi fi-${parsedLang}` : null;
@@ -0,0 +1,31 @@
import Box from "@mui/material/Box";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import BackgroundSVG from "../../../assets/Images/background.svg";
import { useSelector } from "react-redux";
const AppLayout = ({ children }) => {
const theme = useTheme();
const ui = useSelector((state) => state.ui);
return (
<Box
sx={{
minHeight: "100vh",
backgroundColor: theme.palette.primaryBackground.main,
backgroundImage: ui?.mode === "dark" ? `url("${BackgroundSVG}")` : "none",
backgroundSize: "100% 100%",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
color: theme.palette.primary.contrastText,
}}
>
{children}
</Box>
);
};
AppLayout.propTypes = {
children: PropTypes.node,
};
export default AppLayout;
+8 -4
View File
@@ -1,4 +1,6 @@
import { Link as MuiLink, useTheme } from "@mui/material";
import { Link as RouterLink } from "react-router-dom";
import PropTypes from "prop-types";
/**
@@ -10,7 +12,7 @@ import PropTypes from "prop-types";
* @returns {JSX.Element}
*/
const Link = ({ level, label, url }) => {
const Link = ({ level, label, url, external = true }) => {
const theme = useTheme();
const levelConfig = {
@@ -49,11 +51,12 @@ const Link = ({ level, label, url }) => {
const { sx, color } = levelConfig[level];
return (
<MuiLink
href={url}
component={external ? "a" : RouterLink}
to={external ? undefined : url}
href={external ? url : undefined}
sx={{ width: "fit-content", ...sx }}
color={color}
target="_blank"
rel="noreferrer"
{...(external && { target: "_blank", rel: "noreferrer" })}
>
{label}
</MuiLink>
@@ -64,6 +67,7 @@ Link.propTypes = {
url: PropTypes.string.isRequired,
level: PropTypes.oneOf(["primary", "secondary", "tertiary", "error"]),
label: PropTypes.string.isRequired,
external: PropTypes.bool,
};
export default Link;
@@ -0,0 +1,116 @@
import * as React from "react";
import Button from "@mui/material/Button";
import ButtonGroup from "@mui/material/ButtonGroup";
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import ClickAwayListener from "@mui/material/ClickAwayListener";
import Grow from "@mui/material/Grow";
import Paper from "@mui/material/Paper";
import Popper from "@mui/material/Popper";
import MenuItem from "@mui/material/MenuItem";
import MenuList from "@mui/material/MenuList";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { createToast } from "../../Utils/toastUtils";
import { useExportMonitors } from "../../Hooks/monitorHooks";
const MonitorActions = ({ isLoading }) => {
const [open, setOpen] = React.useState(false);
const anchorRef = React.useRef(null);
const [selectedIndex, setSelectedIndex] = React.useState(0);
const navigate = useNavigate();
const { t } = useTranslation();
const [exportMonitors, isExporting] = useExportMonitors();
const options = [t("monitorActions.import"), t("monitorActions.export")];
const handleClick = async () => {
if (selectedIndex === 0) {
// Import
navigate("/uptime/bulk-import");
} else {
// Export
const [success, error] = await exportMonitors();
if (!success) {
createToast({ body: error || t("export.failed") });
}
}
};
const handleMenuItemClick = (event, index) => {
setSelectedIndex(index);
setOpen(false);
};
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
const handleClose = (event) => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
setOpen(false);
};
return (
<React.Fragment>
<ButtonGroup
variant="contained"
color="accent"
ref={anchorRef}
aria-label="Monitor actions"
disabled={isLoading || isExporting}
>
<Button onClick={handleClick}>{options[selectedIndex]}</Button>
<Button
size="small"
aria-controls={open ? "split-button-menu" : undefined}
aria-expanded={open ? "true" : undefined}
aria-label="select monitor action"
aria-haspopup="menu"
onClick={handleToggle}
>
<ArrowDropDownIcon />
</Button>
</ButtonGroup>
<Popper
sx={{ zIndex: 1 }}
open={open}
anchorEl={anchorRef.current}
role={undefined}
transition
disablePortal
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement === "bottom" ? "center top" : "center bottom",
}}
>
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MenuList
id="split-button-menu"
autoFocusItem
>
{options.map((option, index) => (
<MenuItem
key={option}
selected={index === selectedIndex}
onClick={(event) => handleMenuItemClick(event, index)}
>
{option}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</React.Fragment>
);
};
export default MonitorActions;
@@ -4,14 +4,14 @@ import { useTheme } from "@emotion/react";
import SkeletonLayout from "./skeleton";
const MonitorCountHeader = ({
shouldRender = true,
isLoading = false,
monitorCount,
heading = "monitors",
sx,
children,
}) => {
const theme = useTheme();
if (!shouldRender) return <SkeletonLayout />;
if (isLoading) return <SkeletonLayout />;
if (monitorCount === 1) {
heading = "monitor";
@@ -42,7 +42,7 @@ const MonitorCountHeader = ({
};
MonitorCountHeader.propTypes = {
shouldRender: PropTypes.bool,
isLoading: PropTypes.bool,
monitorCount: PropTypes.number,
heading: PropTypes.string,
children: PropTypes.node,
@@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
import MonitorActions from "../MonitorActions";
const CreateMonitorHeader = ({ isAdmin, label, isLoading = true, path, bulkPath }) => {
const navigate = useNavigate();
@@ -28,18 +29,7 @@ const CreateMonitorHeader = ({ isAdmin, label, isLoading = true, path, bulkPath
>
{label || t("createNew")}
</Button>
{bulkPath && (
<Button
loading={isLoading}
variant="contained"
color="accent"
onClick={() => {
navigate(`${bulkPath}`);
}}
>
{t("bulkImport.title")}
</Button>
)}
{/* {bulkPath && <MonitorActions isLoading={isLoading} />} */}
</Stack>
);
};
@@ -2,6 +2,7 @@ import Stack from "@mui/material/Stack";
import Status from "./status";
import Skeleton from "./skeleton";
import Button from "@mui/material/Button";
import { Tooltip } from "@mui/material";
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
import PauseOutlinedIcon from "@mui/icons-material/PauseOutlined";
import PlayArrowOutlinedIcon from "@mui/icons-material/PlayArrowOutlined";
@@ -11,10 +12,10 @@ import EmailIcon from "@mui/icons-material/Email";
import PropTypes from "prop-types";
import { useNavigate } from "react-router-dom";
import { useTheme } from "@mui/material/styles";
import { usePauseMonitor } from "../../Hooks/useMonitorControls";
import { usePauseMonitor } from "../../Hooks/monitorHooks";
import { useSendTestEmail } from "../../Hooks/useSendTestEmail";
import { useTranslation } from "react-i18next";
import { useTestAllNotifications } from "../../Hooks/useNotifications";
/**
* MonitorDetailsControlHeader component displays the control header for monitor details.
* It includes status display, pause/resume button, and a configure button for admins.
@@ -38,12 +39,16 @@ const MonitorDetailsControlHeader = ({
const navigate = useNavigate();
const theme = useTheme();
const { t } = useTranslation();
const [pauseMonitor, isPausing, error] = usePauseMonitor({
monitorId: monitor?._id,
triggerUpdate,
});
const [pauseMonitor, isPausing, error] = usePauseMonitor();
const [isSending, emailError, sendTestEmail] = useSendTestEmail();
const isTestNotificationsDisabled = monitor?.notifications?.length === 0;
const tooltipTitle = isTestNotificationsDisabled ? t("testNotificationsDisabled") : "";
// const [isSending, emailError, sendTestEmail] = useSendTestEmail();
const [testAllNotifications, isSending, errorAllNotifications] =
useTestAllNotifications();
if (isLoading) {
return <Skeleton />;
@@ -60,17 +65,29 @@ const MonitorDetailsControlHeader = ({
direction="row"
gap={theme.spacing(2)}
>
<Button
variant="contained"
color="secondary"
loading={isSending}
startIcon={<EmailIcon />}
onClick={() => {
sendTestEmail();
}}
<Tooltip
key={monitor?._id}
placement="bottom"
title={tooltipTitle}
>
{t("sendTestEmail")}
</Button>
<span>
<Button
variant="contained"
color="secondary"
loading={isSending}
startIcon={<EmailIcon />}
disabled={isTestNotificationsDisabled}
onClick={() => {
testAllNotifications({ monitorId: monitor?._id });
}}
sx={{
whiteSpace: "nowrap",
}}
>
{t("sendTestNotifications")}
</Button>
</span>
</Tooltip>
<Button
variant="contained"
color="secondary"
@@ -80,20 +97,24 @@ const MonitorDetailsControlHeader = ({
>
{t("menu.incidents")}
</Button>
<Button
variant="contained"
color="secondary"
loading={isPausing}
startIcon={
monitor?.isActive ? <PauseOutlinedIcon /> : <PlayArrowOutlinedIcon />
}
onClick={() => {
pauseMonitor();
}}
>
{monitor?.isActive ? "Pause" : "Resume"}
</Button>
{isAdmin && (
<Button
variant="contained"
color="secondary"
loading={isPausing}
startIcon={
monitor?.isActive ? <PauseOutlinedIcon /> : <PlayArrowOutlinedIcon />
}
onClick={() => {
pauseMonitor({
monitorId: monitor?._id,
triggerUpdate,
});
}}
>
{monitor?.isActive ? "Pause" : "Resume"}
</Button>
)}
{isAdmin && (
<Button
variant="contained"
@@ -7,8 +7,8 @@ import Dot from "../../Components/Dot";
import { formatDurationRounded } from "../../Utils/timeUtils";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils";
import { useMonitorUtils } from "../../Hooks/useMonitorUtils";
import { formatMonitorUrl } from "../../Utils/utils";
/**
* Status component displays the status information of a monitor.
* It includes the monitor's name, URL, and check interval.
@@ -23,23 +23,18 @@ import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils";
*/
const Status = ({ monitor }) => {
const theme = useTheme();
const { statusColor, determineState } = useUtils();
const { statusColor, determineState } = useMonitorUtils();
return (
<Stack>
<Typography variant="h1">{monitor?.name}</Typography>
<Typography variant="monitorName">{monitor?.name}</Typography>
<Stack
direction="row"
alignItems={"center"}
gap={theme.spacing(4)}
>
<PulseDot color={statusColor[determineState(monitor)]} />
<Typography
variant="h2"
style={{ fontFamily: "monospace", fontWeight: "bolder" }}
>
{monitor?.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Typography variant="monitorUrl">{formatMonitorUrl(monitor?.url)}</Typography>
<Dot />
<Typography>
Checking every {formatDurationRounded(monitor?.interval)}.
@@ -1,41 +0,0 @@
import { Button, Box } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import SettingsIcon from "../../../assets/icons/settings-bold.svg?react";
import PropTypes from "prop-types";
const ConfigButton = ({ shouldRender = true, monitorId, path }) => {
const theme = useTheme();
const navigate = useNavigate();
if (!shouldRender) return null;
return (
<Box alignSelf="flex-end">
<Button
variant="contained"
color="secondary"
onClick={() => navigate(`/${path}/configure/${monitorId}`)}
sx={{
px: theme.spacing(5),
"& svg": {
mr: theme.spacing(3),
"& path": {
/* Should always be contrastText for the button color */
stroke: theme.palette.secondary.contrastText,
},
},
}}
>
<SettingsIcon /> Configure
</Button>
</Box>
);
};
ConfigButton.propTypes = {
shouldRender: PropTypes.bool,
monitorId: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
};
export default ConfigButton;
@@ -1,59 +0,0 @@
import { Stack, Typography } from "@mui/material";
import PulseDot from "../Animated/PulseDot";
import Dot from "../Dot";
import { useTheme } from "@emotion/react";
import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils";
import { formatDurationRounded } from "../../Utils/timeUtils";
import ConfigButton from "./ConfigButton";
import SkeletonLayout from "./skeleton";
import PropTypes from "prop-types";
const MonitorStatusHeader = ({ path, isLoading = false, isAdmin, monitor }) => {
const theme = useTheme();
const { statusColor, determineState } = useUtils();
if (isLoading) {
return <SkeletonLayout />;
}
return (
<Stack
direction="row"
justifyContent="space-between"
>
<Stack>
<Typography variant="h1">{monitor?.name}</Typography>
<Stack
direction="row"
alignItems={"center"}
gap={theme.spacing(4)}
>
<PulseDot color={statusColor[determineState(monitor)]} />
<Typography
variant="h2"
style={{ fontFamily: "monospace", fontWeight: "bolder" }}
>
{monitor?.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Dot />
<Typography>
Checking every {formatDurationRounded(monitor?.interval)}.
</Typography>
</Stack>
</Stack>
<ConfigButton
path={path}
shouldRender={isAdmin}
monitorId={monitor?._id}
/>
</Stack>
);
};
MonitorStatusHeader.propTypes = {
path: PropTypes.string.isRequired,
isLoading: PropTypes.bool,
isAdmin: PropTypes.bool,
monitor: PropTypes.object,
};
export default MonitorStatusHeader;
@@ -1,23 +0,0 @@
import { Stack, Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Stack
direction="row"
justifyContent="space-between"
>
<Skeleton
height={40}
variant="rounded"
width="15%"
/>
<Skeleton
height={40}
variant="rounded"
width="15%"
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -0,0 +1,106 @@
// Components
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Divider from "@mui/material/Divider";
import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
import Search from "../Inputs/Search";
// Utils
import { useState, useEffect } from "react";
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
const NotificationConfig = ({ notifications, setMonitor, setNotifications }) => {
// Local state
const [notificationsSearch, setNotificationsSearch] = useState("");
const [selectedNotifications, setSelectedNotifications] = useState([]);
const handleSearch = (value) => {
setSelectedNotifications(value);
setMonitor((prev) => {
return {
...prev,
notifications: value.map((notification) => notification._id),
};
});
};
// Handlers
const handleDelete = (id) => {
const updatedNotifications = selectedNotifications.filter(
(notification) => notification._id !== id
);
setSelectedNotifications(updatedNotifications);
setMonitor((prev) => {
return {
...prev,
notifications: updatedNotifications.map((notification) => notification._id),
};
});
};
// Setup
const theme = useTheme();
useEffect(() => {
if (setNotifications) {
const toSet = setNotifications.map((notification) => {
return notifications.find((n) => n._id === notification);
});
setSelectedNotifications(toSet);
}
}, [setNotifications, notifications]);
return (
<Stack gap={theme.spacing(6)}>
<Search
type="notifications"
label="Notifications"
options={notifications}
filteredBy="notificationName"
multiple={true}
value={selectedNotifications}
inputValue={notificationsSearch}
handleInputChange={setNotificationsSearch}
handleChange={(value) => {
handleSearch(value);
}}
/>
<Stack
flex={1}
width="100%"
>
{selectedNotifications.map((notification, index) => (
<Stack
direction="row"
alignItems="center"
key={notification._id}
width="100%"
>
<Typography
flexGrow={1} // <-- This will take up all available horizontal space
>
{notification.notificationName}
</Typography>
<DeleteOutlineRoundedIcon
onClick={() => {
handleDelete(notification._id);
}}
sx={{ cursor: "pointer" }}
/>
{index < selectedNotifications.length - 1 && <Divider />}
</Stack>
))}
</Stack>
</Stack>
);
};
NotificationConfig.propTypes = {
notifications: PropTypes.array,
setMonitor: PropTypes.func,
setNotifications: PropTypes.array,
};
export default NotificationConfig;
@@ -1,30 +0,0 @@
import { Navigate } from "react-router-dom";
import { useSelector } from "react-redux";
import PropTypes from "prop-types";
/**
* @param {Object} props - The props passed to the ProtectedDistributedUptimeRoute component.
* @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 ProtectedDistributedUptimeRoute = ({ children }) => {
const distributedUptimeEnabled = useSelector(
(state) => state.ui.distributedUptimeEnabled
);
return distributedUptimeEnabled === true ? (
children
) : (
<Navigate
to="/uptime"
replace
/>
);
};
ProtectedDistributedUptimeRoute.propTypes = {
children: PropTypes.node.isRequired,
};
export default ProtectedDistributedUptimeRoute;
@@ -0,0 +1,35 @@
import { Navigate } from "react-router-dom";
import { useSelector } from "react-redux";
import PropTypes from "prop-types";
/**
* 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.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 RoleProtectedRoute = ({ roles, children }) => {
const authState = useSelector((state) => state.auth);
const userRoles = authState?.user?.role || [];
const canAccess = userRoles.some((role) => roles.includes(role));
return canAccess ? (
children
) : (
<Navigate
to="/uptime"
replace
/>
);
};
RoleProtectedRoute.propTypes = {
children: PropTypes.element.isRequired,
roles: PropTypes.array,
};
export default RoleProtectedRoute;
+21 -24
View File
@@ -8,7 +8,6 @@ import {
ListItemButton,
ListItemIcon,
ListItemText,
ListSubheader,
Menu,
MenuItem,
Stack,
@@ -23,7 +22,6 @@ 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 Account from "../../assets/icons/user-edit.svg?react";
import Maintenance from "../../assets/icons/maintenance.svg?react";
import Monitors from "../../assets/icons/monitors.svg?react";
import Incidents from "../../assets/icons/incidents.svg?react";
@@ -37,10 +35,11 @@ import ArrowLeft from "../../assets/icons/left-arrow.svg?react";
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 StatusPages from "../../assets/icons/status-pages.svg?react";
import Discussions from "../../assets/icons/discussions.svg?react";
import DistributedUptimeIcon from "../../assets/icons/distributed-uptime.svg?react";
import Notifications from "../../assets/icons/notifications.svg?react";
import Logs from "../../assets/icons/logs.svg?react";
import "./index.css";
// Utils
@@ -50,22 +49,25 @@ import { useDispatch, useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { clearAuthState } from "../../Features/Auth/authSlice";
import { toggleSidebar } from "../../Features/UI/uiSlice";
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
import { TurnedIn } from "@mui/icons-material";
import { rules } from "eslint-plugin-react-refresh";
const getMenu = (t) => [
{ name: t("menu.uptime"), path: "uptime", icon: <Monitors /> },
{ name: t("menu.pagespeed"), path: "pagespeed", icon: <PageSpeed /> },
{ name: t("menu.infrastructure"), path: "infrastructure", icon: <Integrations /> },
{
name: t("menu.distributedUptime"),
path: "distributed-uptime",
icon: <DistributedUptimeIcon />,
name: t("menu.notifications"),
path: "notifications",
icon: <Notifications />,
},
{ name: t("menu.incidents"), path: "incidents", icon: <Incidents /> },
{ name: t("menu.statusPages"), path: "status", icon: <StatusPages /> },
{ name: t("menu.maintenance"), path: "maintenance", icon: <Maintenance /> },
// { name: t("menu.integrations"), path: "integrations", icon: <Integrations /> },
{ name: t("menu.logs"), path: "logs", icon: <Logs /> },
{
name: t("menu.settings"),
icon: <Settings />,
@@ -95,14 +97,13 @@ const URL_MAP = {
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",
changelog: "https://github.com/bluewave-labs/checkmate/releases",
};
const PATH_MAP = {
monitors: "Dashboard",
pagespeed: "Dashboard",
infrastructure: "Dashboard",
["distributed-uptime"]: "Dashboard",
account: "Account",
settings: "Settings",
};
@@ -122,7 +123,6 @@ function Sidebar() {
const { t } = useTranslation();
const authState = useSelector((state) => state.auth);
const menu = getMenu(t);
const otherMenuItems = getOtherMenuItems(t);
const accountMenuItems = getAccountMenuItems(t);
const collapsed = useSelector((state) => state.ui.sidebar.collapsed);
@@ -130,12 +130,17 @@ function Sidebar() {
const [anchorEl, setAnchorEl] = useState(null);
const [popup, setPopup] = useState();
const { user } = useSelector((state) => state.auth);
const distributedUptimeEnabled = useSelector(
(state) => state.ui.distributedUptimeEnabled
);
const sidebarRef = useRef(null);
const [sidebarReady, setSidebarReady] = useState(false);
const TRANSITION_DURATION = 200;
let menu = getMenu(t);
menu = menu.filter((item) => {
if (item.path === "logs") {
return user.role?.includes("admin") || user.role?.includes("superadmin");
}
return true;
});
useEffect(() => {
if (!collapsed) {
@@ -200,7 +205,6 @@ function Sidebar() {
const logout = async () => {
// Clear auth state
dispatch(clearAuthState());
dispatch(clearUptimeMonitorState());
navigate("/login");
};
@@ -234,7 +238,6 @@ function Sidebar() {
borderRight: `1px solid ${theme.palette.primary.lowContrast}`,
borderColor: theme.palette.primary.lowContrast,
borderRadius: 0,
backgroundColor: theme.palette.primary.main,
"& :is(p, span, .MuiListSubheader-root)": {
/*
Text color for unselected menu items and menu headings
@@ -346,7 +349,7 @@ function Sidebar() {
mt={theme.spacing(2)}
sx={{ opacity: 0.8, fontWeight: 500 }}
>
Checkmate
{t("common.appName")}
</Typography>
</Stack>
</Stack>
@@ -375,12 +378,6 @@ function Sidebar() {
}}
>
{menu.map((item) => {
if (
item.path === "distributed-uptime" &&
distributedUptimeEnabled === false
) {
return null;
}
return item.path ? (
/* If item has a path */
<Tooltip
@@ -33,7 +33,6 @@ const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" })
borderBottom: `1px solid ${theme.palette.primary.lowContrast}`,
borderRadius: 0,
gap: theme.spacing(1.5),
backgroundColor: theme.palette.primary.main,
}}
>
<Stack
+4 -4
View File
@@ -2,7 +2,7 @@ import { Stack, Typography } from "@mui/material";
import Image from "../Image";
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils";
import { useMonitorUtils } from "../../Hooks/useMonitorUtils";
/**
* StatBox Component
@@ -41,7 +41,7 @@ const StatBox = ({
sx,
}) => {
const theme = useTheme();
const { statusToTheme } = useUtils();
const { statusToTheme } = useMonitorUtils();
const themeColor = statusToTheme[status];
const statusBoxStyles = gradient
@@ -136,8 +136,8 @@ const StatBox = ({
};
StatBox.propTypes = {
heading: PropTypes.string.isRequired,
subHeading: PropTypes.node.isRequired,
heading: PropTypes.string,
subHeading: PropTypes.node,
gradient: PropTypes.bool,
status: PropTypes.string,
sx: PropTypes.object,
+1 -1
View File
@@ -4,7 +4,7 @@ import SkeletonLayout from "./skeleton";
// Utils
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
const StatusBoxes = ({ shouldRender, flexWrap = "nowrap", children }) => {
const StatusBoxes = ({ shouldRender = true, flexWrap = "nowrap", children }) => {
const theme = useTheme();
if (!shouldRender) {
return (
@@ -4,7 +4,7 @@ import { useTheme } from "@emotion/react";
import { Box, Stack, Typography, Button } from "@mui/material";
import { PasswordEndAdornment } from "../../Inputs/TextInput/Adornments";
import TextInput from "../../Inputs/TextInput";
import { credentials } from "../../../Validation/validation";
import { newOrChangedCredentials } from "../../../Validation/validation";
import Alert from "../../Alert";
import { update } from "../../../Features/Auth/authSlice";
import { useDispatch, useSelector } from "react-redux";
@@ -61,7 +61,7 @@ const PasswordPanel = () => {
[name]: true,
};
const validation = credentials.validate(
const validation = newOrChangedCredentials.validate(
{ ...updatedData },
{ abortEarly: false, context: { password: updatedData.newPassword } }
);
@@ -79,7 +79,7 @@ const PasswordPanel = () => {
const handleSubmit = async (event) => {
event.preventDefault();
const { error } = credentials.validate(localData, {
const { error } = newOrChangedCredentials.validate(localData, {
abortEarly: false,
context: { password: localData.newPassword },
});
@@ -214,7 +214,7 @@ const PasswordPanel = () => {
<TextInput
type="password"
id="edit-confirm-password"
placeholder={t("confirmPassword", "Confirm password")}
placeholder={t("confirmPassword")}
autoComplete="new-password"
value={localData.confirm}
onChange={handleChange}
@@ -5,10 +5,9 @@ import { Box, Button, Divider, Stack, Typography } from "@mui/material";
import Avatar from "../../Avatar";
import TextInput from "../../Inputs/TextInput";
import ImageUpload from "../../Inputs/ImageUpload";
import { credentials } from "../../../Validation/validation";
import { newOrChangedCredentials } from "../../../Validation/validation";
import { useDispatch, useSelector } from "react-redux";
import { clearAuthState, deleteUser, update } from "../../../Features/Auth/authSlice";
import { clearUptimeMonitorState } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
import { GenericDialog } from "../../Dialog/genericDialog";
@@ -58,7 +57,7 @@ const ProfilePanel = () => {
[name]: value,
}));
validateField({ [name]: value }, credentials, name);
validateField({ [name]: value }, newOrChangedCredentials, name);
};
// Validates input against provided schema and updates error state
@@ -162,7 +161,6 @@ const ProfilePanel = () => {
const action = await dispatch(deleteUser());
if (action.payload.success) {
dispatch(clearAuthState());
dispatch(clearUptimeMonitorState());
} else {
if (action.payload) {
// dispatch errors
@@ -240,7 +238,7 @@ const ProfilePanel = () => {
gap={SPACING_GAP}
>
<Stack flex={0.9}>
<Typography component="h1">{t("email")}</Typography>
<Typography component="h1">{t("auth.common.inputs.email.label")}</Typography>
<Typography
component="p"
sx={{ opacity: 0.6 }}
@@ -4,7 +4,7 @@ import { Button, ButtonGroup, Stack, Typography } from "@mui/material";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import TextInput from "../../Inputs/TextInput";
import { credentials } from "../../../Validation/validation";
import { newOrChangedCredentials } from "../../../Validation/validation";
import { networkService } from "../../../main";
import { createToast } from "../../../Utils/toastUtils";
import Select from "../../Inputs/Select";
@@ -115,7 +115,10 @@ const TeamPanel = () => {
email: newEmail,
}));
const validation = credentials.validate({ email: newEmail }, { abortEarly: false });
const validation = newOrChangedCredentials.validate(
{ email: newEmail },
{ abortEarly: false }
);
setErrors((prev) => {
const updatedErrors = { ...prev };
@@ -142,7 +145,7 @@ const TeamPanel = () => {
if (!toInvite.role.includes("user") || !toInvite.role.includes("admin"))
setToInvite((prev) => ({ ...prev, role: ["user"] }));
const { error } = credentials.validate(
const { error } = newOrChangedCredentials.validate(
{ email: toInvite.email },
{
abortEarly: false,
@@ -165,7 +168,7 @@ const TeamPanel = () => {
});
} catch (error) {
createToast({
body: error.message || "Unknown error.",
body: error?.response?.data?.msg || error.message || "Unknown error.",
});
} finally {
setIsSendingInvite(false);
@@ -265,7 +268,7 @@ const TeamPanel = () => {
value={toInvite.email}
onChange={handleChange}
error={errors.email ? true : false}
helperText={errors.email}
helperText={t(errors.email)}
/>
<Select
id="team-member-role"
+11
View File
@@ -0,0 +1,11 @@
export const createHeaderFactory = (getCellSx = () => {}) => {
return ({ id, content, onClick = () => {}, render = () => {} }) => {
return {
id,
content,
onClick,
getCellSx,
render,
};
};
};
+36
View File
@@ -0,0 +1,36 @@
import Typography from "@mui/material/Typography";
import Stack from "@mui/material/Stack";
import Link from "@mui/material/Link";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Link as RouterLink } from "react-router-dom";
const TextLink = ({ text, linkText, href, target = "_self" }) => {
const theme = useTheme();
return (
<Stack
direction="row"
gap={theme.spacing(4)}
>
<Typography>{text}</Typography>
<Link
color="accent"
to={href}
component={RouterLink}
target={target}
>
{linkText}
</Link>
</Stack>
);
};
TextLink.propTypes = {
text: PropTypes.string,
linkText: PropTypes.string,
href: PropTypes.string,
target: PropTypes.string,
};
export default TextLink;
+3 -1
View File
@@ -14,10 +14,12 @@ import SunAndMoonIcon from "./SunAndMoonIcon";
import { useDispatch, useSelector } from "react-redux";
import { setMode } from "../../Features/UI/uiSlice";
import "./index.css";
import { useTranslation } from "react-i18next";
const ThemeSwitch = ({ width = 48, height = 48, color }) => {
const mode = useSelector((state) => state.ui.mode);
const dispatch = useDispatch();
const { t } = useTranslation();
const toggleTheme = () => {
dispatch(setMode(mode === "light" ? "dark" : "light"));
@@ -26,7 +28,7 @@ const ThemeSwitch = ({ width = 48, height = 48, color }) => {
return (
<IconButton
id="theme-toggle"
title="Toggles light & dark"
title={t("common.buttons.toggleTheme")}
className={`theme-${mode}`}
aria-label="auto"
aria-live="polite"
@@ -1,389 +0,0 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { networkService } from "../../main";
const initialState = {
isLoading: false,
monitorsSummary: [],
success: null,
msg: null,
};
export const createInfrastructureMonitor = createAsyncThunk(
"infrastructureMonitors/createMonitor",
async (data, thunkApi) => {
try {
const { monitor } = data;
const res = await networkService.createMonitor({ monitor: monitor });
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const checkInfrastructureEndpointResolution = createAsyncThunk(
"infrastructureMonitors/CheckEndpoint",
async (data, thunkApi) => {
try {
const { monitorURL } = data;
const res = await networkService.checkEndpointResolution({
monitorURL: monitorURL,
});
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const getInfrastructureMonitorById = createAsyncThunk(
"infrastructureMonitors/getMonitorById",
async (data, thunkApi) => {
try {
const { monitorId } = data;
const res = await networkService.getMonitorById({ monitorId: monitorId });
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const getInfrastructureMonitorsByTeamId = createAsyncThunk(
"infrastructureMonitors/getMonitorsByTeamId",
async (_, thunkApi) => {
const user = thunkApi.getState().auth.user;
try {
const res = await networkService.getMonitorsAndSummaryByTeamId({
teamId: user.teamId,
types: ["hardware"],
limit: 1,
rowsPerPage: 0,
});
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const updateInfrastructureMonitor = createAsyncThunk(
"infrastructureMonitors/updateMonitor",
async ({ monitorId, monitor }, thunkApi) => {
try {
const updatedFields = {
name: monitor.name,
description: monitor.description,
interval: monitor.interval,
notifications: monitor.notifications,
thresholds: monitor.thresholds,
secret: monitor.secret,
};
const res = await networkService.updateMonitor({
monitorId,
monitor,
updatedFields,
});
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const deleteInfrastructureMonitor = createAsyncThunk(
"infrastructureMonitors/deleteMonitor",
async (data, thunkApi) => {
try {
const { monitor } = data;
const res = await networkService.deleteMonitorById({ monitorId: monitor._id });
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const pauseInfrastructureMonitor = createAsyncThunk(
"infrastructureMonitors/pauseMonitor",
async (data, thunkApi) => {
try {
const { monitorId } = data;
const res = await networkService.pauseMonitorById({ monitorId: monitorId });
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const deleteInfrastructureMonitorChecksByTeamId = createAsyncThunk(
"infrastructureMonitors/deleteChecksByTeamId",
async (data, thunkApi) => {
try {
const { teamId } = data;
const res = await networkService.deleteChecksByTeamId({ teamId: teamId });
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const deleteAllInfrastructureMonitors = createAsyncThunk(
"infrastructureMonitors/deleteAllMonitors",
async (data, thunkApi) => {
try {
const res = await networkService.deleteAllMonitors();
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
const infrastructureMonitorsSlice = createSlice({
name: "infrastructureMonitors",
initialState,
reducers: {
clearInfrastructureMonitorState: (state) => {
state.isLoading = false;
state.monitorsSummary = [];
state.success = null;
state.msg = null;
},
},
extraReducers: (builder) => {
builder
// *****************************************************
// Monitors by teamId
// *****************************************************
.addCase(getInfrastructureMonitorsByTeamId.pending, (state) => {
state.isLoading = true;
})
.addCase(getInfrastructureMonitorsByTeamId.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.msg;
state.monitorsSummary = action.payload.data;
})
.addCase(getInfrastructureMonitorsByTeamId.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Getting infrastructure monitors failed";
})
// *****************************************************
// Create Monitor
// *****************************************************
.addCase(createInfrastructureMonitor.pending, (state) => {
state.isLoading = true;
})
.addCase(createInfrastructureMonitor.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(createInfrastructureMonitor.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Failed to create infrastructure monitor";
})
// *****************************************************
// Resolve Endpoint
// *****************************************************
.addCase(checkInfrastructureEndpointResolution.pending, (state) => {
state.isLoading = true;
})
.addCase(checkInfrastructureEndpointResolution.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(checkInfrastructureEndpointResolution.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Failed to check endpoint resolution";
})
// *****************************************************
// Get Monitor By Id
// *****************************************************
.addCase(getInfrastructureMonitorById.pending, (state) => {
state.isLoading = true;
})
.addCase(getInfrastructureMonitorById.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(getInfrastructureMonitorById.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Failed to get infrastructure monitor";
})
// *****************************************************
// update Monitor
// *****************************************************
.addCase(updateInfrastructureMonitor.pending, (state) => {
state.isLoading = true;
})
.addCase(updateInfrastructureMonitor.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(updateInfrastructureMonitor.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Failed to update infrastructure monitor";
})
// *****************************************************
// Delete Monitor
// *****************************************************
.addCase(deleteInfrastructureMonitor.pending, (state) => {
state.isLoading = true;
})
.addCase(deleteInfrastructureMonitor.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(deleteInfrastructureMonitor.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Failed to delete infrastructure monitor";
})
// *****************************************************
// Delete Monitor checks by Team ID
// *****************************************************
.addCase(deleteInfrastructureMonitorChecksByTeamId.pending, (state) => {
state.isLoading = true;
})
.addCase(deleteInfrastructureMonitorChecksByTeamId.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(deleteInfrastructureMonitorChecksByTeamId.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Failed to delete monitor checks";
})
// *****************************************************
// Pause Monitor
// *****************************************************
.addCase(pauseInfrastructureMonitor.pending, (state) => {
state.isLoading = true;
})
.addCase(pauseInfrastructureMonitor.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(pauseInfrastructureMonitor.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Failed to pause infrastructure monitor";
})
// *****************************************************
// Delete all Monitors
// *****************************************************
.addCase(deleteAllInfrastructureMonitors.pending, (state) => {
state.isLoading = true;
})
.addCase(deleteAllInfrastructureMonitors.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(deleteAllInfrastructureMonitors.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload ? action.payload.msg : "Failed to delete all monitors";
});
},
});
export const { setInfrastructureMonitors, clearInfrastructureMonitorState } =
infrastructureMonitorsSlice.actions;
export default infrastructureMonitorsSlice.reducer;
@@ -1,309 +0,0 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { networkService } from "../../main";
const initialState = {
isLoading: false,
monitorsSummary: [],
success: null,
msg: null,
};
export const createPageSpeed = createAsyncThunk(
"pageSpeedMonitors/createPageSpeed",
async (data, thunkApi) => {
try {
const { monitor } = data;
const res = await networkService.createMonitor({ monitor: monitor });
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const checkEndpointResolution = createAsyncThunk(
"monitors/checkEndpoint",
async (data, thunkApi) => {
try {
const { monitorURL } = data;
const res = await networkService.checkEndpointResolution({
monitorURL: monitorURL,
});
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const getPagespeedMonitorById = createAsyncThunk(
"monitors/getMonitorById",
async (data, thunkApi) => {
try {
const { monitorId } = data;
const res = await networkService.getMonitorById({ monitorId: monitorId });
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const getPageSpeedByTeamId = createAsyncThunk(
"pageSpeedMonitors/getPageSpeedByTeamId",
async (_, thunkApi) => {
const user = thunkApi.getState().auth.user;
try {
const res = await networkService.getMonitorsAndSummaryByTeamId({
teamId: user.teamId,
types: ["pagespeed"],
});
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const updatePageSpeed = createAsyncThunk(
"pageSpeedMonitors/updatePageSpeed",
async (data, thunkApi) => {
try {
const { monitor } = data;
const updatedFields = {
name: monitor.name,
description: monitor.description,
interval: monitor.interval,
notifications: monitor.notifications,
};
const res = await networkService.updateMonitor({
monitorId: monitor._id,
updatedFields: updatedFields,
});
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const deletePageSpeed = createAsyncThunk(
"pageSpeedMonitors/deletePageSpeed",
async (data, thunkApi) => {
try {
const { monitor } = data;
const res = await networkService.deleteMonitorById({ monitorId: monitor._id });
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const pausePageSpeed = createAsyncThunk(
"pageSpeedMonitors/pausePageSpeed",
async (data, thunkApi) => {
try {
const { monitorId } = data;
const res = await networkService.pauseMonitorById({ monitorId: monitorId });
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
const pageSpeedMonitorSlice = createSlice({
name: "pageSpeedMonitor",
initialState,
reducers: {
clearMonitorState: (state) => {
state.isLoading = false;
state.monitorsSummary = [];
state.success = null;
state.msg = null;
},
},
extraReducers: (builder) => {
builder
// *****************************************************
// Monitors by teamId
// *****************************************************
.addCase(getPageSpeedByTeamId.pending, (state) => {
state.isLoading = true;
})
.addCase(getPageSpeedByTeamId.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.msg;
state.monitorsSummary = action.payload.data;
})
.addCase(getPageSpeedByTeamId.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Getting page speed monitors failed";
})
// *****************************************************
.addCase(getPagespeedMonitorById.pending, (state) => {
state.isLoading = true;
})
.addCase(getPagespeedMonitorById.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(getPagespeedMonitorById.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Failed to get pagespeed monitor";
})
// *****************************************************
// Create Monitor
// *****************************************************
.addCase(createPageSpeed.pending, (state) => {
state.isLoading = true;
})
.addCase(createPageSpeed.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(createPageSpeed.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Failed to create page speed monitor";
})
// *****************************************************
// Resolve Endpoint
// *****************************************************
.addCase(checkEndpointResolution.pending, (state) => {
state.isLoading = true;
})
.addCase(checkEndpointResolution.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(checkEndpointResolution.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Failed to check endpoint resolution";
})
// *****************************************************
// Update Monitor
// *****************************************************
.addCase(updatePageSpeed.pending, (state) => {
state.isLoading = true;
})
.addCase(updatePageSpeed.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(updatePageSpeed.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Failed to update page speed monitor";
})
// *****************************************************
// Delete Monitor
// *****************************************************
.addCase(deletePageSpeed.pending, (state) => {
state.isLoading = true;
})
.addCase(deletePageSpeed.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(deletePageSpeed.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Failed to delete page speed monitor";
})
// *****************************************************
// Pause Monitor
// *****************************************************
.addCase(pausePageSpeed.pending, (state) => {
state.isLoading = true;
})
.addCase(pausePageSpeed.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(pausePageSpeed.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Failed to pause page speed monitor";
});
},
});
export const { setMonitors, clearMonitorState } = pageSpeedMonitorSlice.actions;
export default pageSpeedMonitorSlice.reducer;
@@ -1,101 +0,0 @@
import { networkService } from "../../main";
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
const initialState = {
isLoading: false,
apiBaseUrl: "",
logLevel: "debug",
language: "gb",
pagespeedApiKey: "",
};
export const getAppSettings = createAsyncThunk(
"settings/getSettings",
async (data, thunkApi) => {
try {
const res = await networkService.getAppSettings();
return res.data;
} catch (error) {
if (error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const updateAppSettings = createAsyncThunk(
"settings/updateSettings",
async ({ settings }, thunkApi) => {
try {
const parsedSettings = {
language: settings.language,
pagespeedApiKey: settings.pagespeedApiKey,
};
const res = await networkService.updateAppSettings({ settings: parsedSettings });
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
const handleGetSettingsFulfilled = (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
state.apiBaseUrl = action.payload.data.apiBaseUrl;
state.logLevel = action.payload.data.logLevel;
state.language = action.payload.data.language;
state.pagespeedApiKey = action.payload.data.pagespeedApiKey;
};
const handleGetSettingsRejected = (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload ? action.payload.msg : "Failed to get settings.";
};
const handleUpdateSettingsFulfilled = (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
state.apiBaseUrl = action.payload.data.apiBaseUrl;
state.logLevel = action.payload.data.logLevel;
};
const handleUpdateSettingsRejected = (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload ? action.payload.msg : "Failed to update settings.";
};
const settingsSlice = createSlice({
name: "settings",
initialState,
extraReducers: (builder) => {
builder
.addCase(getAppSettings.pending, (state) => {
state.isLoading = true;
})
.addCase(getAppSettings.fulfilled, handleGetSettingsFulfilled)
.addCase(getAppSettings.rejected, handleGetSettingsRejected);
builder
.addCase(updateAppSettings.pending, (state) => {
state.isLoading = true;
})
.addCase(updateAppSettings.fulfilled, handleUpdateSettingsFulfilled)
.addCase(updateAppSettings.rejected, handleUpdateSettingsRejected);
},
});
export default settingsSlice.reducer;
+5 -2
View File
@@ -16,6 +16,9 @@ const initialState = {
maintenance: {
rowsPerPage: 5,
},
infrastructure: {
rowsPerPage: 5,
},
sidebar: {
collapsed: false,
},
@@ -24,7 +27,7 @@ const initialState = {
greeting: { index: 0, lastUpdate: null },
timezone: "America/Toronto",
distributedUptimeEnabled: false,
language: "gb",
language: "en",
starPromptOpen: true,
};
@@ -57,7 +60,7 @@ const uiSlice = createSlice({
setTimezone(state, action) {
state.timezone = action.payload.timezone;
},
setLanguage(state, action) {
setLanguage: (state, action) => {
state.language = action.payload;
},
setStarPromptOpen: (state, action) => {
@@ -1,380 +0,0 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { networkService } from "../../main";
const initialState = {
isLoading: false,
monitorsSummary: [],
success: null,
msg: null,
};
export const createUptimeMonitor = createAsyncThunk(
"monitors/createMonitor",
async (data, thunkApi) => {
try {
const { monitor } = data;
const res = await networkService.createMonitor({ monitor: monitor });
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const checkEndpointResolution = createAsyncThunk(
"monitors/checkEndpoint",
async (data, thunkApi) => {
try {
const { monitorURL } = data;
const res = await networkService.checkEndpointResolution({
monitorURL: monitorURL,
});
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const getUptimeMonitorById = createAsyncThunk(
"monitors/getMonitorById",
async (data, thunkApi) => {
try {
const { monitorId } = data;
const res = await networkService.getMonitorById({ monitorId: monitorId });
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const updateUptimeMonitor = createAsyncThunk(
"monitors/updateMonitor",
async (data, thunkApi) => {
try {
const { monitor } = data;
const updatedFields = {
name: monitor.name,
description: monitor.description,
interval: monitor.interval,
notifications: monitor.notifications,
matchMethod: monitor.matchMethod,
expectedValue: monitor.expectedValue,
ignoreTlsErrors: monitor.ignoreTlsErrors,
jsonPath: monitor.jsonPath,
...(monitor.type === "port" && { port: monitor.port }),
};
const res = await networkService.updateMonitor({
monitorId: monitor._id,
updatedFields,
});
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const deleteUptimeMonitor = createAsyncThunk(
"monitors/deleteMonitor",
async (data, thunkApi) => {
try {
const { monitor } = data;
const res = await networkService.deleteMonitorById({ monitorId: monitor._id });
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const pauseUptimeMonitor = createAsyncThunk(
"monitors/pauseMonitor",
async (data, thunkApi) => {
try {
const { monitorId } = data;
const res = await networkService.pauseMonitorById({ monitorId: monitorId });
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const deleteMonitorChecksByTeamId = createAsyncThunk(
"monitors/deleteChecksByTeamId",
async (data, thunkApi) => {
try {
const { teamId } = data;
const res = await networkService.deleteChecksByTeamId({ teamId: teamId });
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const addDemoMonitors = createAsyncThunk(
"monitors/addDemoMonitors",
async (data, thunkApi) => {
try {
const res = await networkService.addDemoMonitors();
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const deleteAllMonitors = createAsyncThunk(
"monitors/deleteAllMonitors",
async (data, thunkApi) => {
try {
const res = await networkService.deleteAllMonitors();
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
const uptimeMonitorsSlice = createSlice({
name: "uptimeMonitors",
initialState,
reducers: {
clearUptimeMonitorState: (state) => {
state.isLoading = false;
state.monitorsSummary = [];
state.success = null;
state.msg = null;
},
},
extraReducers: (builder) => {
builder
// *****************************************************
// Create Monitor
// *****************************************************
.addCase(createUptimeMonitor.pending, (state) => {
state.isLoading = true;
})
.addCase(createUptimeMonitor.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(createUptimeMonitor.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Failed to create uptime monitor";
})
// *****************************************************
// Resolve Endpoint
// *****************************************************
.addCase(checkEndpointResolution.pending, (state) => {
state.isLoading = true;
})
.addCase(checkEndpointResolution.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(checkEndpointResolution.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Failed to check endpoint resolution";
})
// *****************************************************
// Get Monitor By Id
// *****************************************************
.addCase(getUptimeMonitorById.pending, (state) => {
state.isLoading = true;
})
.addCase(getUptimeMonitorById.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(getUptimeMonitorById.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload ? action.payload.msg : "Failed to get uptime monitor";
})
// *****************************************************
// update Monitor
// *****************************************************
.addCase(updateUptimeMonitor.pending, (state) => {
state.isLoading = true;
})
.addCase(updateUptimeMonitor.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(updateUptimeMonitor.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Failed to update uptime monitor";
})
// *****************************************************
// Delete Monitor
// *****************************************************
.addCase(deleteUptimeMonitor.pending, (state) => {
state.isLoading = true;
})
.addCase(deleteUptimeMonitor.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(deleteUptimeMonitor.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Failed to delete uptime monitor";
})
// *****************************************************
// Delete Monitor checks by Team ID
// *****************************************************
.addCase(deleteMonitorChecksByTeamId.pending, (state) => {
state.isLoading = true;
})
.addCase(deleteMonitorChecksByTeamId.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(deleteMonitorChecksByTeamId.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Failed to delete monitor checks";
})
// *****************************************************
// Pause Monitor
// *****************************************************
.addCase(pauseUptimeMonitor.pending, (state) => {
state.isLoading = true;
})
.addCase(pauseUptimeMonitor.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(pauseUptimeMonitor.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Failed to pause uptime monitor";
})
// *****************************************************
// Add Demo Monitors
// *****************************************************
.addCase(addDemoMonitors.pending, (state) => {
state.isLoading = true;
})
.addCase(addDemoMonitors.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(addDemoMonitors.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Failed to add demo uptime monitors";
})
// *****************************************************
// Delete all Monitors
// *****************************************************
.addCase(deleteAllMonitors.pending, (state) => {
state.isLoading = true;
})
.addCase(deleteAllMonitors.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(deleteAllMonitors.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload ? action.payload.msg : "Failed to delete all monitors";
});
},
});
export const { setUptimeMonitors, clearUptimeMonitorState } = uptimeMonitorsSlice.actions;
export default uptimeMonitorsSlice.reducer;
+213
View File
@@ -0,0 +1,213 @@
import { useState, useEffect } from "react";
import { networkService } from "../main";
import { createToast } from "../Utils/toastUtils";
import { useTranslation } from "react-i18next";
const useFetchChecksTeam = ({
status,
sortOrder,
limit,
dateRange,
filter,
ack,
page,
rowsPerPage,
enabled = true,
updateTrigger,
}) => {
const [checks, setChecks] = useState(undefined);
const [checksCount, setChecksCount] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
const [networkError, setNetworkError] = useState(false);
useEffect(() => {
const fetchChecks = async () => {
if (!enabled) {
return;
}
const config = {
status,
sortOrder,
limit,
dateRange,
filter,
ack,
page,
rowsPerPage,
};
try {
setIsLoading(true);
const res = await networkService.getChecksByTeam(config);
setChecks(res.data.data.checks);
setChecksCount(res.data.data.checksCount);
} catch (error) {
setNetworkError(true);
createToast({ body: error.message });
} finally {
setIsLoading(false);
}
};
fetchChecks();
}, [
status,
sortOrder,
limit,
dateRange,
filter,
ack,
page,
rowsPerPage,
enabled,
updateTrigger,
]);
return [checks, checksCount, isLoading, networkError];
};
const useFetchChecksByMonitor = ({
monitorId,
type,
status,
sortOrder,
limit,
dateRange,
filter,
ack,
page,
rowsPerPage,
enabled = true,
updateTrigger,
}) => {
const [checks, setChecks] = useState(undefined);
const [checksCount, setChecksCount] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
const [networkError, setNetworkError] = useState(false);
useEffect(() => {
const fetchChecks = async () => {
if (!enabled || !type) {
return;
}
const config = {
monitorId,
type,
status,
sortOrder,
limit,
dateRange,
filter,
ack,
page,
rowsPerPage,
};
try {
setIsLoading(true);
const res = await networkService.getChecksByMonitor(config);
setChecks(res.data.data.checks);
setChecksCount(res.data.data.checksCount);
} catch (error) {
setNetworkError(true);
createToast({ body: error.message });
} finally {
setIsLoading(false);
}
};
fetchChecks();
}, [
monitorId,
type,
status,
sortOrder,
limit,
dateRange,
filter,
ack,
page,
rowsPerPage,
enabled,
updateTrigger,
]);
return [checks, checksCount, isLoading, networkError];
};
const useFetchChecksSummaryByTeamId = ({ updateTrigger } = {}) => {
const [summary, setSummary] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
const [networkError, setNetworkError] = useState(false);
useEffect(() => {
const fetchSummary = async () => {
try {
setIsLoading(true);
const res = await networkService.getChecksAndSummaryByTeamId();
setSummary(res.data.data);
} catch (error) {
setNetworkError(true);
createToast({ body: error.message });
} finally {
setIsLoading(false);
}
};
fetchSummary();
}, [updateTrigger]);
return [summary, isLoading, networkError];
};
const useResolveIncident = () => {
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const resolveIncident = async (checkId, setUpdateTrigger) => {
try {
setIsLoading(true);
await networkService.updateCheckStatus({
checkId,
ack: true,
});
setUpdateTrigger((prev) => !prev);
} catch (error) {
createToast({ body: t("checkHooks.failureResolveOne") });
} finally {
setIsLoading(false);
}
};
return [resolveIncident, isLoading];
};
const useAckAllChecks = () => {
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const ackAllChecks = async (setUpdateTrigger) => {
try {
setIsLoading(true);
await networkService.updateAllChecksStatus({ ack: true });
setUpdateTrigger((prev) => !prev);
} catch (error) {
createToast({ body: t("checkHooks.failureResolveAll") });
} finally {
setIsLoading(false);
}
};
return [ackAllChecks, isLoading];
};
export {
useFetchChecksByMonitor,
useFetchChecksTeam,
useFetchChecksSummaryByTeamId,
useResolveIncident,
useAckAllChecks,
};
+35
View File
@@ -0,0 +1,35 @@
import { useState, useEffect } from "react";
import { networkService } from "../main";
import { createToast } from "../Utils/toastUtils";
import { useTranslation } from "react-i18next";
const useFetchLogs = () => {
const { t } = useTranslation();
const [logs, setLogs] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
useEffect(() => {
const fetchLogs = async () => {
try {
setIsLoading(true);
const response = await networkService.getLogs();
setLogs(response.data.data);
createToast({
body: t("logsPage.toast.fetchLogsSuccess"),
});
} catch (error) {
setError(error);
createToast({
message: error.message,
});
} finally {
setIsLoading(false);
}
};
fetchLogs();
}, [t]);
return [logs, isLoading, error];
};
export { useFetchLogs };
+506
View File
@@ -0,0 +1,506 @@
import { useEffect, useState } from "react";
import { networkService } from "../main";
import { createToast } from "../Utils/toastUtils";
import { useTheme } from "@emotion/react";
import { useMonitorUtils } from "./useMonitorUtils";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
const useFetchMonitorsWithSummary = ({ types, monitorUpdateTrigger }) => {
const [isLoading, setIsLoading] = useState(false);
const [monitors, setMonitors] = useState(undefined);
const [monitorsSummary, setMonitorsSummary] = useState(undefined);
const [networkError, setNetworkError] = useState(false);
useEffect(() => {
const fetchMonitors = async () => {
try {
setIsLoading(true);
const res = await networkService.getMonitorsWithSummaryByTeamId({
types,
});
const { monitors, summary } = res?.data?.data ?? {};
setMonitors(monitors);
setMonitorsSummary(summary);
} catch (error) {
console.error(error);
setNetworkError(true);
createToast({
body: error.message,
});
} finally {
setIsLoading(false);
}
};
fetchMonitors();
}, [types, monitorUpdateTrigger]);
return [monitors, monitorsSummary, isLoading, networkError];
};
const useFetchMonitorsWithChecks = ({
types,
limit,
page,
rowsPerPage,
filter,
field,
order,
monitorUpdateTrigger,
}) => {
const [isLoading, setIsLoading] = useState(false);
const [count, setCount] = useState(undefined);
const [monitors, setMonitors] = useState(undefined);
const [networkError, setNetworkError] = useState(false);
const theme = useTheme();
const { getMonitorWithPercentage } = useMonitorUtils();
useEffect(() => {
const fetchMonitors = async () => {
try {
setIsLoading(true);
const res = await networkService.getMonitorsWithChecksByTeamId({
limit,
types,
page,
rowsPerPage,
filter,
field,
order,
});
const { count, monitors } = res?.data?.data ?? {};
const mappedMonitors = monitors.map((monitor) =>
getMonitorWithPercentage(monitor, theme)
);
setMonitors(mappedMonitors);
setCount(count?.monitorsCount ?? 0);
} catch (error) {
console.error(error);
setNetworkError(true);
createToast({
body: error.message,
});
} finally {
setIsLoading(false);
}
};
fetchMonitors();
}, [
field,
filter,
getMonitorWithPercentage,
limit,
order,
page,
rowsPerPage,
theme,
types,
monitorUpdateTrigger,
]);
return [monitors, count, isLoading, networkError];
};
const useFetchMonitorsByTeamId = ({
types,
limit,
page,
rowsPerPage,
filter,
field,
order,
checkOrder,
normalize,
status,
updateTrigger,
}) => {
const [isLoading, setIsLoading] = useState(false);
const [monitors, setMonitors] = useState(undefined);
const [summary, setSummary] = useState(undefined);
const [networkError, setNetworkError] = useState(false);
useEffect(() => {
const fetchMonitors = async () => {
try {
setIsLoading(true);
const res = await networkService.getMonitorsByTeamId({
limit,
types,
page,
rowsPerPage,
filter,
field,
order,
checkOrder,
status,
normalize,
});
if (res?.data?.data?.filteredMonitors) {
setMonitors(res.data.data.filteredMonitors);
setSummary(res.data.data.summary);
}
} catch (error) {
setNetworkError(true);
createToast({
body: error.message,
});
} finally {
setIsLoading(false);
}
};
fetchMonitors();
}, [
types,
limit,
page,
rowsPerPage,
filter,
field,
order,
updateTrigger,
checkOrder,
normalize,
status,
]);
return [monitors, summary, isLoading, networkError];
};
const useFetchStatsByMonitorId = ({
monitorId,
sortOrder,
limit,
dateRange,
numToDisplay,
normalize,
updateTrigger,
}) => {
const [monitor, setMonitor] = useState(undefined);
const [audits, setAudits] = useState(undefined);
const [isLoading, setIsLoading] = useState(true);
const [networkError, setNetworkError] = useState(false);
useEffect(() => {
const fetchMonitor = async () => {
try {
setIsLoading(true);
const res = await networkService.getStatsByMonitorId({
monitorId: monitorId,
sortOrder,
limit,
dateRange,
numToDisplay,
normalize,
});
setMonitor(res?.data?.data ?? undefined);
setAudits(res?.data?.data?.checks?.[0]?.audits ?? undefined);
} catch (error) {
setNetworkError(true);
createToast({ body: error.message });
} finally {
setIsLoading(false);
}
};
fetchMonitor();
}, [monitorId, dateRange, numToDisplay, normalize, sortOrder, limit, updateTrigger]);
return [monitor, audits, isLoading, networkError];
};
const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchMonitor = async () => {
try {
setIsLoading(true);
const res = await networkService.getMonitorById({ monitorId: monitorId });
setMonitor(res.data.data);
} catch (error) {
createToast({ body: error.message });
} finally {
setIsLoading(false);
}
};
fetchMonitor();
}, [monitorId, setMonitor, updateTrigger]);
return [isLoading];
};
const useFetchHardwareMonitorById = ({ monitorId, dateRange, updateTrigger }) => {
const [isLoading, setIsLoading] = useState(true);
const [networkError, setNetworkError] = useState(false);
const [monitor, setMonitor] = useState(undefined);
useEffect(() => {
const fetchMonitor = async () => {
try {
if (!monitorId) {
return { monitor: undefined, isLoading: false, networkError: undefined };
}
const response = await networkService.getHardwareDetailsByMonitorId({
monitorId: monitorId,
dateRange: dateRange,
});
setMonitor(response.data.data);
} catch (error) {
setNetworkError(true);
} finally {
setIsLoading(false);
}
};
fetchMonitor();
}, [monitorId, dateRange, updateTrigger]);
return [monitor, isLoading, networkError];
};
const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => {
const [networkError, setNetworkError] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [monitor, setMonitor] = useState(undefined);
const [monitorStats, setMonitorStats] = useState(undefined);
useEffect(() => {
const fetchMonitors = async () => {
try {
const res = await networkService.getUptimeDetailsById({
monitorId: monitorId,
dateRange: dateRange,
normalize: true,
});
const { monitorData, monitorStats } = res?.data?.data ?? {};
setMonitor(monitorData);
setMonitorStats(monitorStats);
} catch (error) {
setNetworkError(true);
createToast({ body: error.message });
} finally {
setIsLoading(false);
}
};
fetchMonitors();
}, [dateRange, monitorId, trigger]);
return [monitor, monitorStats, isLoading, networkError];
};
const useCreateMonitor = () => {
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const createMonitor = async ({ monitor, redirect }) => {
try {
setIsLoading(true);
await networkService.createMonitor({ monitor });
createToast({ body: "Monitor created successfully!" });
if (redirect) {
navigate(redirect);
}
} catch (error) {
createToast({ body: "Failed to create monitor." });
} finally {
setIsLoading(false);
}
};
return [createMonitor, isLoading];
};
const useDeleteMonitor = () => {
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const deleteMonitor = async ({ monitor, redirect }) => {
try {
setIsLoading(true);
await networkService.deleteMonitorById({ monitorId: monitor._id });
createToast({ body: "Monitor deleted successfully!" });
if (redirect) {
navigate(redirect);
}
} catch (error) {
createToast({ body: "Failed to delete monitor." });
} finally {
setIsLoading(false);
}
};
return [deleteMonitor, isLoading];
};
const useUpdateMonitor = () => {
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const updateMonitor = async ({ monitor, redirect }) => {
try {
setIsLoading(true);
const updatedFields = {
name: monitor.name,
description: monitor.description,
interval: monitor.interval,
notifications: monitor.notifications,
matchMethod: monitor.matchMethod,
expectedValue: monitor.expectedValue,
ignoreTlsErrors: monitor.ignoreTlsErrors,
jsonPath: monitor.jsonPath,
...(monitor.type === "port" && { port: monitor.port }),
...(monitor.type === "hardware" && {
thresholds: monitor.thresholds,
secret: monitor.secret,
}),
};
await networkService.updateMonitor({
monitorId: monitor._id,
updatedFields,
});
createToast({ body: "Monitor updated successfully!" });
if (redirect) {
navigate(redirect);
}
} catch (error) {
createToast({ body: "Failed to update monitor." });
} finally {
setIsLoading(false);
}
};
return [updateMonitor, isLoading];
};
const usePauseMonitor = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
const pauseMonitor = async ({ monitorId, triggerUpdate }) => {
try {
setIsLoading(true);
const res = await networkService.pauseMonitorById({ monitorId });
createToast({
body: res.data.data.isActive
? "Monitor resumed successfully"
: "Monitor paused successfully",
});
triggerUpdate();
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
};
return [pauseMonitor, isLoading, error];
};
const useAddDemoMonitors = () => {
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const addDemoMonitors = async () => {
try {
setIsLoading(true);
await networkService.addDemoMonitors();
createToast({ body: t("monitorHooks.successAddDemoMonitors") });
} catch (error) {
createToast({ body: t("monitorHooks.failureAddDemoMonitors") });
} finally {
setIsLoading(false);
}
};
return [addDemoMonitors, isLoading];
};
const useDeleteAllMonitors = () => {
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const deleteAllMonitors = async () => {
try {
setIsLoading(true);
await networkService.deleteAllMonitors();
createToast({ body: t("settingsMonitorsDeleted") });
} catch (error) {
createToast({ body: t("settingsFailedToDeleteMonitors") });
} finally {
setIsLoading(false);
}
};
return [deleteAllMonitors, isLoading];
};
const useDeleteMonitorStats = () => {
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const deleteMonitorStats = async () => {
setIsLoading(true);
try {
await networkService.deleteChecksByTeamId();
createToast({ body: t("settingsStatsCleared") });
} catch (error) {
createToast({ body: t("settingsFailedToClearStats") });
} finally {
setIsLoading(false);
}
};
return [deleteMonitorStats, isLoading];
};
const useCreateBulkMonitors = () => {
const [isLoading, setIsLoading] = useState(false);
const createBulkMonitors = async (file, user) => {
setIsLoading(true);
const formData = new FormData();
formData.append("csvFile", file);
try {
const response = await networkService.createBulkMonitors(formData);
return [true, response.data, null]; // [success, data, error]
} catch (err) {
const errorMessage = err?.response?.data?.msg || err.message;
return [false, null, errorMessage];
} finally {
setIsLoading(false);
}
};
return [createBulkMonitors, isLoading];
};
const useExportMonitors = () => {
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const exportMonitors = async () => {
setIsLoading(true);
try {
const response = await networkService.exportMonitors();
// Create a download link
const url = window.URL.createObjectURL(response.data);
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", "monitors.csv");
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
createToast({ body: t("export.success") });
return [true, null];
} catch (err) {
const errorMessage = err?.response?.data?.msg || err.message;
createToast({ body: errorMessage || t("export.failed") });
return [false, errorMessage];
} finally {
setIsLoading(false);
}
};
return [exportMonitors, isLoading];
};
export {
useFetchMonitorsWithSummary,
useFetchMonitorsWithChecks,
useFetchMonitorsByTeamId,
useFetchStatsByMonitorId,
useFetchMonitorById,
useFetchUptimeMonitorById,
useFetchHardwareMonitorById,
useCreateMonitor,
useDeleteMonitor,
useUpdateMonitor,
usePauseMonitor,
useAddDemoMonitors,
useDeleteAllMonitors,
useDeleteMonitorStats,
useCreateBulkMonitors,
useExportMonitors,
};
+57
View File
@@ -0,0 +1,57 @@
import { useState, useEffect } from "react";
import { networkService } from "../main";
import { createToast } from "../Utils/toastUtils";
const useFetchQueueData = (trigger) => {
const [jobs, setJobs] = useState(undefined);
const [metrics, setMetrics] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
useEffect(() => {
const fetchJobs = async () => {
try {
setIsLoading(true);
const response = await networkService.getQueueData();
if (response.status === 200) {
setJobs(response.data.data.jobs);
setMetrics(response.data.data.metrics);
}
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
};
fetchJobs();
}, [trigger]);
return [jobs, metrics, isLoading, error];
};
const useFlushQueue = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
const flushQueue = async (trigger, setTrigger) => {
try {
setIsLoading(true);
await networkService.flushQueue();
createToast({
body: "Queue flushed",
});
} catch (error) {
setError(error);
createToast({
body: error.message,
});
} finally {
setIsLoading(false);
setTrigger(!trigger);
}
};
return [flushQueue, isLoading, error];
};
export { useFetchQueueData, useFlushQueue };
@@ -3,7 +3,7 @@ import { networkService } from "../main";
import { createToast } from "../Utils/toastUtils";
import { useTranslation } from "react-i18next";
const useFetchSettings = ({ setSettingsData }) => {
const useFetchSettings = ({ setSettingsData, setIsApiKeySet, setIsEmailPasswordSet }) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
useEffect(() => {
@@ -12,6 +12,8 @@ const useFetchSettings = ({ setSettingsData }) => {
try {
const response = await networkService.getAppSettings();
setSettingsData(response?.data?.data);
setIsApiKeySet(response?.data?.data?.pagespeedKeySet);
setIsEmailPasswordSet(response?.data?.data?.emailPasswordSet);
} catch (error) {
createToast({ body: "Failed to fetch settings" });
setError(error);
@@ -20,12 +22,18 @@ const useFetchSettings = ({ setSettingsData }) => {
}
};
fetchSettings();
}, []);
}, [setSettingsData]);
return [isLoading, error];
};
const useSaveSettings = () => {
const useSaveSettings = ({
setSettingsData,
setIsApiKeySet,
setApiKeyHasBeenReset,
setIsEmailPasswordSet,
setEmailPasswordHasBeenReset,
}) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
const { t } = useTranslation();
@@ -33,12 +41,21 @@ const useSaveSettings = () => {
const saveSettings = async (settings) => {
setIsLoading(true);
try {
await networkService.updateAppSettings({ settings });
const settingsResponse = await networkService.updateAppSettings({ settings });
if (settings.checkTTL) {
await networkService.updateChecksTTL({
ttl: settings.checkTTL,
});
}
setIsApiKeySet(settingsResponse.data.data.pagespeedKeySet);
setIsEmailPasswordSet(settingsResponse.data.data.emailPasswordSet);
if (settingsResponse.data.data.pagespeedKeySet === true) {
setApiKeyHasBeenReset(false);
}
if (settingsResponse.data.data.emailPasswordSet === true) {
setEmailPasswordHasBeenReset(false);
}
setSettingsData(settingsResponse.data.data);
createToast({ body: t("settingsSuccessSaved") });
} catch (error) {
createToast({ body: t("settingsFailedToSave") });
-27
View File
@@ -1,27 +0,0 @@
import { useState } from "react";
import { networkService } from "../main";
export const useBulkMonitors = () => {
const [isLoading, setIsLoading] = useState(false);
const createBulkMonitors = async (file, user) => {
setIsLoading(true);
const formData = new FormData();
formData.append("csvFile", file);
formData.append("userId", user._id);
formData.append("teamId", user.teamId);
try {
const response = await networkService.createBulkMonitors(formData);
return [true, response.data, null]; // [success, data, error]
} catch (err) {
const errorMessage = err?.response?.data?.msg || err.message;
return [false, null, errorMessage];
} finally {
setIsLoading(false);
}
};
return [createBulkMonitors, isLoading];
};
-24
View File
@@ -1,24 +0,0 @@
import { useState } from "react";
import { networkService } from "../main";
import { createToast } from "../Utils/toastUtils";
import { useTranslation } from "react-i18next";
const UseDeleteMonitorStats = () => {
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const deleteMonitorStats = async ({ teamId }) => {
setIsLoading(true);
try {
const res = await networkService.deleteChecksByTeamId({ teamId });
createToast({ body: t("settingsStatsCleared") });
} catch (error) {
createToast({ body: t("settingsFailedToClearStats") });
} finally {
setIsLoading(false);
}
};
return [deleteMonitorStats, isLoading];
};
export { UseDeleteMonitorStats };
@@ -1,58 +0,0 @@
import { useState, useEffect } from "react";
import { networkService } from "../main";
import { createToast } from "../Utils/toastUtils";
import { useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import { useMonitorUtils } from "./useMonitorUtils";
const useFetchDepinStatusPage = ({ url, timeFrame, isCreate = false }) => {
const [isLoading, setIsLoading] = useState(true);
const [networkError, setNetworkError] = useState(false);
const [statusPage, setStatusPage] = useState(undefined);
const [monitorId, setMonitorId] = useState(undefined);
const [isPublished, setIsPublished] = useState(false);
const { authToken } = useSelector((state) => state.auth);
const theme = useTheme();
const { getMonitorWithPercentage } = useMonitorUtils();
useEffect(() => {
if (isCreate) return;
const fetchStatusPageByUrl = async () => {
try {
const response = await networkService.getDistributedStatusPageByUrl({
authToken,
url,
type: "distributed",
timeFrame,
});
if (!response?.data?.data) return;
const statusPage = response.data.data;
const monitorsWithPercentage = statusPage?.subMonitors.map((monitor) =>
getMonitorWithPercentage(monitor, theme)
);
const statusPageWithSubmonitorPercentages = {
...statusPage,
subMonitors: monitorsWithPercentage,
};
setStatusPage(statusPageWithSubmonitorPercentages);
setMonitorId(statusPage?.monitors[0]);
setIsPublished(statusPage?.isPublished);
} catch (error) {
setNetworkError(true);
createToast({
body: error.message,
});
} finally {
setIsLoading(false);
}
};
fetchStatusPageByUrl();
}, [authToken, url, getMonitorWithPercentage, theme, timeFrame, isCreate]);
return [isLoading, networkError, statusPage, monitorId, isPublished];
};
export { useFetchDepinStatusPage };
@@ -1,72 +0,0 @@
import { useEffect, useState } from "react";
import { networkService } from "../main";
import { createToast } from "../Utils/toastUtils";
import { useTheme } from "@emotion/react";
import { useMonitorUtils } from "./useMonitorUtils";
export const useFetchMonitorsWithChecks = ({
teamId,
types,
limit,
page,
rowsPerPage,
filter,
field,
order,
monitorUpdateTrigger,
}) => {
const [isLoading, setIsLoading] = useState(false);
const [count, setCount] = useState(undefined);
const [monitors, setMonitors] = useState(undefined);
const [networkError, setNetworkError] = useState(false);
const theme = useTheme();
const { getMonitorWithPercentage } = useMonitorUtils();
useEffect(() => {
const fetchMonitors = async () => {
try {
setIsLoading(true);
const res = await networkService.getMonitorsWithChecksByTeamId({
teamId,
limit,
types,
page,
rowsPerPage,
filter,
field,
order,
});
const { count, monitors } = res?.data?.data ?? {};
const mappedMonitors = monitors.map((monitor) =>
getMonitorWithPercentage(monitor, theme)
);
setMonitors(mappedMonitors);
setCount(count?.monitorsCount ?? 0);
} catch (error) {
console.error(error);
setNetworkError(true);
createToast({
body: error.message,
});
} finally {
setIsLoading(false);
}
};
fetchMonitors();
}, [
field,
filter,
getMonitorWithPercentage,
limit,
order,
page,
rowsPerPage,
teamId,
theme,
types,
monitorUpdateTrigger,
]);
return [monitors, count, isLoading, networkError];
};
export default useFetchMonitorsWithChecks;
@@ -1,37 +0,0 @@
import { useEffect, useState } from "react";
import { networkService } from "../main";
import { createToast } from "../Utils/toastUtils";
export const useFetchMonitorsWithSummary = ({ teamId, types, monitorUpdateTrigger }) => {
const [isLoading, setIsLoading] = useState(false);
const [monitors, setMonitors] = useState(undefined);
const [monitorsSummary, setMonitorsSummary] = useState(undefined);
const [networkError, setNetworkError] = useState(false);
useEffect(() => {
const fetchMonitors = async () => {
try {
setIsLoading(true);
const res = await networkService.getMonitorsWithSummaryByTeamId({
teamId,
types,
});
const { monitors, summary } = res?.data?.data ?? {};
setMonitors(monitors);
setMonitorsSummary(summary);
} catch (error) {
console.error(error);
setNetworkError(true);
createToast({
body: error.message,
});
} finally {
setIsLoading(false);
}
};
fetchMonitors();
}, [teamId, types, monitorUpdateTrigger]);
return [monitors, monitorsSummary, isLoading, networkError];
};
export default useFetchMonitorsWithSummary;
@@ -1,35 +0,0 @@
import { useEffect, useState } from "react";
import { networkService } from "../main";
import { useNavigate } from "react-router-dom";
import { createToast } from "../Utils/toastUtils";
export const useFetchUptimeMonitorDetails = ({ monitorId, dateRange, trigger }) => {
const [networkError, setNetworkError] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [monitor, setMonitor] = useState(undefined);
const [monitorStats, setMonitorStats] = useState(undefined);
const navigate = useNavigate();
useEffect(() => {
const fetchMonitors = async () => {
try {
const res = await networkService.getUptimeDetailsById({
monitorId: monitorId,
dateRange: dateRange,
normalize: true,
});
const { monitorData, monitorStats } = res?.data?.data ?? {};
setMonitor(monitorData);
setMonitorStats(monitorStats);
} catch (error) {
setNetworkError(true);
createToast({ body: error.message });
} finally {
setIsLoading(false);
}
};
fetchMonitors();
}, [dateRange, monitorId, navigate, trigger]);
return [monitor, monitorStats, isLoading, networkError];
};
export default useFetchUptimeMonitorDetails;
-28
View File
@@ -1,28 +0,0 @@
import { useState } from "react";
import { networkService } from "../main";
import { createToast } from "../Utils/toastUtils";
const usePauseMonitor = ({ monitorId, triggerUpdate }) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
const pauseMonitor = async () => {
try {
setIsLoading(true);
const res = await networkService.pauseMonitorById({ monitorId });
createToast({
body: res.data.data.isActive
? "Monitor resumed successfully"
: "Monitor paused successfully",
});
triggerUpdate();
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
};
return [pauseMonitor, isLoading, error];
};
export { usePauseMonitor };
+21 -1
View File
@@ -46,7 +46,27 @@ const useMonitorUtils = () => {
pending: theme.palette.warning.lowContrast,
};
return { getMonitorWithPercentage, determineState, statusColor };
const statusToTheme = {
up: "success",
down: "error",
paused: "warning",
pending: "secondary",
"cannot resolve": "tertiary",
};
const pagespeedStatusMsg = {
up: "Live (collecting data)",
down: "Inactive",
paused: "Paused",
};
return {
getMonitorWithPercentage,
determineState,
statusColor,
statusToTheme,
pagespeedStatusMsg,
};
};
export { useMonitorUtils };
+208
View File
@@ -0,0 +1,208 @@
import { useState, useEffect, useCallback } from "react";
import { createToast } from "../Utils/toastUtils";
import { networkService } from "../main";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { NOTIFICATION_TYPES } from "../Pages/Notifications/utils";
const useCreateNotification = () => {
const navigate = useNavigate();
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const createNotification = async (notification) => {
try {
setIsLoading(true);
await networkService.createNotification({ notification });
createToast({
body: t("notifications.create.success"),
});
navigate("/notifications");
} catch (error) {
setError(error);
createToast({
body: t("notifications.create.failed"),
});
} finally {
setIsLoading(false);
}
};
return [createNotification, isLoading, error];
};
const useGetNotificationsByTeamId = (updateTrigger) => {
const [notifications, setNotifications] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const { t } = useTranslation();
const getNotifications = useCallback(async () => {
try {
setIsLoading(true);
const response = await networkService.getNotificationsByTeamId();
setNotifications(response?.data?.data ?? []);
} catch (error) {
setError(error);
createToast({
body: t("notifications.fetch.failed"),
});
} finally {
setIsLoading(false);
}
}, [t]);
useEffect(() => {
getNotifications();
}, [getNotifications, updateTrigger]);
return [notifications, isLoading, error];
};
const useDeleteNotification = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const { t } = useTranslation();
const deleteNotification = async (id, callback) => {
try {
setIsLoading(true);
await networkService.deleteNotificationById({ id });
createToast({
body: t("notifications.delete.success"),
});
if (callback) {
callback();
}
} catch (error) {
setError(error);
createToast({
body: t("notifications.delete.failed"),
});
} finally {
setIsLoading(false);
}
};
return [deleteNotification, isLoading, error];
};
const useGetNotificationById = (id, setNotification) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const getNotificationById = useCallback(async () => {
try {
setIsLoading(true);
const response = await networkService.getNotificationById({ id });
const notification = response?.data?.data ?? null;
const notificationData = {
address: notification?.address,
notificationName: notification?.notificationName,
type: NOTIFICATION_TYPES.find((type) => type.value === notification?.type)?._id,
};
setNotification(notificationData);
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
}, [id, setNotification]);
useEffect(() => {
if (id) {
getNotificationById();
}
}, [getNotificationById, id]);
return [isLoading, error];
};
const useEditNotification = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const { t } = useTranslation();
const navigate = useNavigate();
const editNotification = async (id, notification) => {
try {
setIsLoading(true);
await networkService.editNotification({ id, notification });
createToast({
body: t("notifications.edit.success"),
});
navigate(`/notifications`);
} catch (error) {
setError(error);
createToast({
body: t("notifications.edit.failed"),
});
} finally {
setIsLoading(false);
}
};
return [editNotification, isLoading, error];
};
const useTestNotification = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const { t } = useTranslation();
const testNotification = async (notification) => {
try {
setIsLoading(true);
await networkService.testNotification({ notification });
createToast({
body: t("notifications.test.success"),
});
} catch (error) {
setError(error);
createToast({
body: error?.response?.data?.msg || t("notifications.test.failed"),
});
} finally {
setIsLoading(false);
}
};
return [testNotification, isLoading, error];
};
const useTestAllNotifications = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
const { t } = useTranslation();
const testAllNotifications = async ({ monitorId }) => {
try {
setIsLoading(true);
await networkService.testAllNotifications({ monitorId });
createToast({
body: t("notifications.test.success"),
});
} catch (error) {
createToast({
body: error.response?.data?.msg || error.message,
});
setError(error);
} finally {
setIsLoading(false);
}
};
return [testAllNotifications, isLoading, error];
};
export {
useCreateNotification,
useGetNotificationsByTeamId,
useDeleteNotification,
useGetNotificationById,
useEditNotification,
useTestNotification,
useTestAllNotifications,
};
@@ -1,96 +0,0 @@
import { useState, useEffect } from "react";
import { networkService } from "../main";
import { createToast } from "../Utils/toastUtils";
const useSubscribeToDepinDetails = ({ monitorId, isPublic, isPublished, dateRange }) => {
const [isLoading, setIsLoading] = useState(true);
const [connectionStatus, setConnectionStatus] = useState(undefined);
const [retryCount, setRetryCount] = useState(0);
const [networkError, setNetworkError] = useState(false);
const [monitor, setMonitor] = useState(undefined);
const [lastUpdateTrigger, setLastUpdateTrigger] = useState(0);
useEffect(() => {
if (typeof monitorId === "undefined") {
return;
}
// If this page is public and not published, don't subscribe to details
if (isPublic && isPublished === false) {
return;
}
// Get initial data
const fetchInitialData = async () => {
try {
const res = await networkService.getDistributedUptimeDetails({
monitorId,
dateRange: dateRange,
normalize: true,
isPublic,
});
const responseData = res?.data?.data;
if (typeof responseData === "undefined") {
throw new Error("No data");
}
setConnectionStatus("up");
setLastUpdateTrigger(Date.now());
setMonitor(responseData);
} catch (error) {
setNetworkError(true);
setConnectionStatus("down");
createToast({
body: error.message,
});
} finally {
setIsLoading(false);
}
};
fetchInitialData();
try {
const cleanup = networkService.subscribeToDistributedUptimeDetails({
monitorId,
dateRange: dateRange,
normalize: true,
onUpdate: (data) => {
if (isLoading === true) {
setIsLoading(false);
}
if (networkError === true) {
setNetworkError(false);
}
setLastUpdateTrigger(Date.now());
setMonitor(data.monitor);
},
onOpen: () => {
setConnectionStatus("up");
setRetryCount(0); // Reset retry count on successful connection
},
onError: () => {
setIsLoading(false);
setNetworkError(true);
setConnectionStatus("down");
},
});
return cleanup;
} catch (error) {
setNetworkError(true);
}
}, [
dateRange,
monitorId,
retryCount,
setConnectionStatus,
networkError,
isLoading,
isPublic,
isPublished,
]);
return [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger];
};
export { useSubscribeToDepinDetails };
@@ -1,95 +0,0 @@
import { useEffect, useState } from "react";
import { networkService } from "../main";
import { useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import { useMonitorUtils } from "./useMonitorUtils";
import { createToast } from "../Utils/toastUtils";
const useSubscribeToDepinMonitors = (page, rowsPerPage) => {
// Redux
const { user } = useSelector((state) => state.auth);
// Local state
const [isLoading, setIsLoading] = useState(true);
const [networkError, setNetworkError] = useState(false);
const [count, setCount] = useState(undefined);
const [monitors, setMonitors] = useState(undefined);
const theme = useTheme();
const { getMonitorWithPercentage } = useMonitorUtils();
useEffect(() => {
const fetchInitialData = async () => {
try {
const initialDataRes = await networkService.getDistributedUptimeMonitors({
teamId: user.teamId,
limit: 25,
types: ["distributed_http"],
page,
rowsPerPage,
});
const { count, monitors } = initialDataRes?.data?.data ?? {};
const responseData = initialDataRes?.data?.data;
if (typeof responseData === "undefined") throw new Error("No data");
const mappedMonitors = monitors?.map((monitor) =>
getMonitorWithPercentage(monitor, theme)
);
setMonitors(mappedMonitors);
setCount(count?.monitorsCount ?? 0);
} catch (error) {
setNetworkError(true);
createToast({
body: error.message,
});
} finally {
setIsLoading(false);
}
};
fetchInitialData();
try {
const cleanup = networkService.subscribeToDistributedUptimeMonitors({
teamId: user.teamId,
limit: 25,
types: [
typeof import.meta.env.VITE_DEPIN_TESTING === "undefined"
? "distributed_http"
: "distributed_test",
],
page,
rowsPerPage,
filter: null,
field: null,
order: null,
onUpdate: (data) => {
if (isLoading === true) {
setIsLoading(false);
}
const { count, monitors } = data;
const mappedMonitors = monitors.map((monitor) =>
getMonitorWithPercentage(monitor, theme)
);
setMonitors(mappedMonitors);
setCount(count?.monitorsCount ?? 0);
},
onError: () => {
setIsLoading(false);
setNetworkError(true);
},
});
return cleanup;
} catch (error) {
createToast({
body: error.message,
});
setNetworkError(true);
}
}, [user, getMonitorWithPercentage, theme, isLoading, page, rowsPerPage]);
return [monitors, count, isLoading, networkError];
};
export { useSubscribeToDepinMonitors };
+1 -1
View File
@@ -1,6 +1,6 @@
import { useState } from "react";
import PropTypes from "prop-types";
import { useNavigate } from "react-router";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { Box, Tab, useTheme } from "@mui/material";
import CustomTabList from "../../Components/Tab";
+58 -42
View File
@@ -2,10 +2,10 @@ import { Box, Button, Stack, Typography } from "@mui/material";
import { useEffect, useState } from "react";
import { useTheme } from "@emotion/react";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router";
import { useNavigate } from "react-router-dom";
import { createToast } from "../../Utils/toastUtils";
import { forgotPassword } from "../../Features/Auth/authSlice";
import { useTranslation } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import Background from "../../assets/Images/background-grid.svg?react";
import EmailIcon from "../../assets/icons/email.svg?react";
import Logo from "../../assets/icons/checkmate-icon.svg?react";
@@ -31,16 +31,16 @@ const CheckEmail = () => {
const toastFail = [
{
body: "Email not found.",
body: t("auth.forgotPassword.toasts.emailNotFound"),
},
{
body: "Redirecting in 3...",
body: t("auth.forgotPassword.toasts.redirect").replace("<seconds/>", "3"),
},
{
body: "Redirecting in 2...",
body: t("auth.forgotPassword.toasts.redirect").replace("<seconds/>", "2"),
},
{
body: "Redirecting in 1...",
body: t("auth.forgotPassword.toasts.redirect").replace("<seconds/>", "1"),
},
];
@@ -62,19 +62,19 @@ const CheckEmail = () => {
const action = await dispatch(forgotPassword(form));
if (action.payload.success) {
createToast({
body: `Instructions sent to ${form.email}.`,
body: t("auth.forgotPassword.toasts.sent").replace("<email/>", form.email),
});
setDisabled(false);
} else {
if (action.payload) {
// dispatch errors
createToast({
body: action.payload.msg,
body: action.payload.msg, // FIXME: Potential untranslated string
});
} else {
// unknown errors
createToast({
body: "Unknown error.",
body: t("common.toasts.unknownError"),
});
}
}
@@ -118,7 +118,7 @@ const CheckEmail = () => {
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>Checkmate</Typography>
<Typography sx={{ userSelect: "none" }}>{t("common.appName")}</Typography>
</Stack>
<Stack
width="100%"
@@ -160,18 +160,24 @@ const CheckEmail = () => {
svgHeight={24}
mb={theme.spacing(4)}
>
<EmailIcon alt="email icon" />
<EmailIcon alt={t("auth.forgotPassword.imageAlts.email")} />
</IconBox>
</Stack>
<Typography component="h1">{t("authCheckEmailTitle")}</Typography>
<Typography component="h1">{t("auth.forgotPassword.heading")}</Typography>
<Typography>
{t("authCheckEmailDescription")}{" "}
<Typography
className="email-sent-to"
component="span"
>
{email || "username@email.com"}
</Typography>
<Trans
i18nKey="auth.forgotPassword.subheadings.stepTwo"
components={{
email: (
<Typography
className="email-sent-to"
component="span"
>
{email || "username@email.com"}
</Typography>
),
}}
/>
</Typography>
</Box>
<Button
@@ -183,23 +189,27 @@ const CheckEmail = () => {
maxWidth: 400,
}}
>
{t("authCheckEmailOpenEmailButton")}
{t("auth.forgotPassword.buttons.openEmail")}
</Button>
<Typography sx={{ alignSelf: "center", mt: theme.spacing(6) }}>
{t("authCheckEmailDidntReceiveEmail")}{" "}
<Typography
component="span"
onClick={resendToken}
sx={{
color: theme.palette.accent.main,
userSelect: "none",
pointerEvents: disabled ? "none" : "auto",
cursor: disabled ? "default" : "pointer",
opacity: disabled ? 0.5 : 1,
<Trans
i18nKey="auth.forgotPassword.links.resend"
components={{
a: (
<Typography
component="span"
onClick={resendToken}
sx={{
color: theme.palette.accent.main,
userSelect: "none",
pointerEvents: disabled ? "none" : "auto",
cursor: disabled ? "default" : "pointer",
opacity: disabled ? 0.5 : 1,
}}
/>
),
}}
>
{t("authCheckEmailClickToResend")}
</Typography>
/>
</Typography>
</Stack>
</Stack>
@@ -207,15 +217,21 @@ const CheckEmail = () => {
textAlign="center"
p={theme.spacing(12)}
>
<Typography display="inline-block">{t("goBackTo")}</Typography>
<Typography
component="span"
ml={theme.spacing(2)}
color={theme.palette.accent.main}
onClick={handleNavigate}
sx={{ userSelect: "none" }}
>
{t("authLoginTitle")}
<Typography display="inline-block">
<Trans
i18nKey="auth.forgotPassword.links.login"
components={{
a: (
<Typography
component="span"
color={theme.palette.accent.main}
ml={theme.spacing(2)}
onClick={handleNavigate}
sx={{ userSelect: "none" }}
/>
),
}}
/>
</Typography>
</Box>
</Stack>
+35 -26
View File
@@ -4,14 +4,14 @@ import { createToast } from "../../Utils/toastUtils";
import { useDispatch, useSelector } from "react-redux";
import { forgotPassword } from "../../Features/Auth/authSlice";
import { useEffect, useState } from "react";
import { credentials } from "../../Validation/validation";
import { newOrChangedCredentials } from "../../Validation/validation";
import { useNavigate } from "react-router-dom";
import TextInput from "../../Components/Inputs/TextInput";
import Logo from "../../assets/icons/checkmate-icon.svg?react";
import Key from "../../assets/icons/key.svg?react";
import Background from "../../assets/Images/background-grid.svg?react";
import IconBox from "../../Components/IconBox";
import { useTranslation } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import "./index.css";
const ForgotPassword = () => {
@@ -35,14 +35,14 @@ const ForgotPassword = () => {
const handleSubmit = async (event) => {
event.preventDefault();
const { error } = credentials.validate(form, { abortEarly: false });
const { error } = newOrChangedCredentials.validate(form, { abortEarly: false });
if (error) {
// validation errors
const err =
error.details && error.details.length > 0
? error.details[0].message
: "Error validating data.";
? error.details[0].message // FIXME: Possibly untranslated string
: t("auth.common.errors.validation");
setErrors({ email: err });
createToast({
body: err,
@@ -53,18 +53,18 @@ const ForgotPassword = () => {
sessionStorage.setItem("email", form.email);
navigate("/check-email");
createToast({
body: `Instructions sent to ${form.email}.`,
body: t("auth.forgotPassword.toasts.sent").replace("<email/>", form.email),
});
} else {
if (action.payload) {
// dispatch errors
createToast({
body: action.payload.msg,
body: action.payload.msg, // FIXME: Potentially untranslated string
});
} else {
// unknown errors
createToast({
body: "Unknown error.",
body: t("common.toasts.unknownError"),
});
}
}
@@ -75,7 +75,10 @@ const ForgotPassword = () => {
const { value } = event.target;
setForm({ email: value });
const { error } = credentials.validate({ email: value }, { abortEarly: false });
const { error } = newOrChangedCredentials.validate(
{ email: value },
{ abortEarly: false }
);
if (error) setErrors({ email: error.details[0].message });
else delete errors.email;
@@ -120,7 +123,7 @@ const ForgotPassword = () => {
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>Checkmate</Typography>
<Typography sx={{ userSelect: "none" }}>{t("common.appName")}</Typography>
</Stack>
<Stack
width="100%"
@@ -162,11 +165,11 @@ const ForgotPassword = () => {
svgHeight={24}
mb={theme.spacing(4)}
>
<Key alt="password key icon" />
<Key alt={t("auth.forgotPassword.imageAlts.passwordKey")} />
</IconBox>
</Stack>
<Typography component="h1">{t("authForgotPasswordTitle")}</Typography>
<Typography>{t("authForgotPasswordInstructions")}</Typography>
<Typography component="h1">{t("auth.forgotPassword.heading")}</Typography>
<Typography>{t("auth.forgotPassword.subheadings.stepOne")}</Typography>
</Box>
<Box
component="form"
@@ -179,13 +182,13 @@ const ForgotPassword = () => {
<TextInput
type="email"
id="forgot-password-email-input"
label={t("email")}
label={t("auth.common.inputs.email.label")}
isRequired={true}
placeholder={t("enterEmail")}
placeholder={t("auth.common.inputs.email.placeholder")}
value={form.email}
onChange={handleChange}
error={errors.email ? true : false}
helperText={t(errors.email)}
helperText={t(errors.email)} // Localization keys are in validation.js
/>
<Button
variant="contained"
@@ -198,7 +201,7 @@ const ForgotPassword = () => {
mt: theme.spacing(15),
}}
>
{t("continue")}
{t("auth.common.navigation.continue")}
</Button>
</Box>
</Stack>
@@ -207,15 +210,21 @@ const ForgotPassword = () => {
textAlign="center"
p={theme.spacing(12)}
>
<Typography display="inline-block">{t("goBackTo")}</Typography>
<Typography
component="span"
color={theme.palette.accent.main}
ml={theme.spacing(2)}
onClick={handleNavigate}
sx={{ userSelect: "none" }}
>
{t("authLoginTitle")}
<Typography display="inline-block">
<Trans
i18nKey="auth.forgotPassword.links.login"
components={{
a: (
<Typography
component="span"
color={theme.palette.accent.main}
ml={theme.spacing(2)}
onClick={handleNavigate}
sx={{ userSelect: "none" }}
/>
),
}}
/>
</Typography>
</Box>
</Stack>
@@ -1,107 +0,0 @@
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";
import { useTranslation } from "react-i18next";
/**
* 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);
const { t } = useTranslation();
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">{t("authLoginTitle")}</Typography>
<Typography>{t("authLoginEnterEmail")}</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={t("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 ? t(errors.email) : ""}
ref={inputRef}
/>
<Stack
direction="row"
justifyContent="flex-end"
>
<Button
variant="contained"
color="accent"
type="submit"
disabled={errors.email && true}
className="dashboard-style-button"
sx={{
width: "30%",
px: theme.spacing(6),
borderRadius: `${theme.shape.borderRadius}px !important`,
"&.MuiButtonBase-root": {
borderRadius: `${theme.shape.borderRadius}px !important`,
},
"&.MuiButton-root": {
borderRadius: `${theme.shape.borderRadius}px !important`,
},
"&.Mui-focusVisible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
boxShadow: `none`,
},
}}
>
{t("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;
@@ -1,45 +0,0 @@
import { Box, Typography, useTheme } from "@mui/material";
import PropTypes from "prop-types";
import { useNavigate } from "react-router";
import { useTranslation } from "react-i18next";
const ForgotPasswordLabel = ({ email, errorEmail }) => {
const theme = useTheme();
const navigate = useNavigate();
const { t } = useTranslation();
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}
>
{t("authForgotPasswordTitle")}
</Typography>
<Typography
component="span"
color={theme.palette.accent.main}
ml={theme.spacing(2)}
sx={{ userSelect: "none" }}
onClick={handleNavigate}
>
{t("authForgotPasswordResetPassword")}
</Typography>
</Box>
);
};
ForgotPasswordLabel.propTypes = {
email: PropTypes.string.isRequired,
errorEmail: PropTypes.string.isRequired,
};
export default ForgotPasswordLabel;
@@ -1,141 +0,0 @@
import { useRef, useEffect } from "react";
import { Box, Button, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
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";
import { useTranslation } from "react-i18next";
/**
* 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);
const { t } = useTranslation();
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">{t("authLoginTitle")}</Typography>
<Typography>{t("authLoginEnterPassword")}</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={t("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}
className="dashboard-style-button"
sx={{
px: theme.spacing(5),
borderRadius: `${theme.shape.borderRadius}px !important`,
"&.MuiButtonBase-root": {
borderRadius: `${theme.shape.borderRadius}px !important`,
},
"&.MuiButton-root": {
borderRadius: `${theme.shape.borderRadius}px !important`,
},
"& svg.MuiSvgIcon-root": {
mr: theme.spacing(3),
},
"&:focus-visible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
},
}}
>
<ArrowBackRoundedIcon />
{t("commonBack")}{" "}
</Button>
<Button
variant="contained"
color="accent"
type="submit"
loading={authState.isLoading}
disabled={errors.password && true}
className="dashboard-style-button"
sx={{
width: "30%",
px: theme.spacing(4),
borderRadius: `${theme.shape.borderRadius}px !important`,
"&.MuiButtonBase-root": {
borderRadius: `${theme.shape.borderRadius}px !important`,
},
"&.MuiButton-root": {
borderRadius: `${theme.shape.borderRadius}px !important`,
},
"&.Mui-focusVisible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
},
boxShadow: `none`,
}}
>
{t("continue")}
</Button>
</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;
-262
View File
@@ -1,262 +0,0 @@
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/checkmate-icon.svg?react";
import "../index.css";
import EmailStep from "./Components/EmailStep";
import PasswordStep from "./Components/PasswordStep";
import ThemeSwitch from "../../../Components/ThemeSwitch";
import ForgotPasswordLabel from "./Components/ForgotPasswordLabel";
import LanguageSelector from "../../../Components/LanguageSelector";
import { useTranslation } from "react-i18next";
const DEMO = import.meta.env.VITE_APP_DEMO;
/**
* Displays the login page.
*/
const Login = () => {
const theme = useTheme();
const dispatch = useDispatch();
const { t } = useTranslation();
const navigate = useNavigate();
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");
}
}, [authToken, navigate]);
const handleChange = (event) => {
const { value, id } = event.target;
const name = idMap[id];
const lowerCasedValue =
name === idMap["login-email-input"] ? value?.toLowerCase() || value : value;
setForm((prev) => ({
...prev,
[name]: lowerCasedValue,
}));
const { error } = credentials.validate(
{ [name]: lowerCasedValue },
{ 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) {
const errorMessage = error.details[0].message;
const translatedMessage = errorMessage.startsWith("auth")
? t(errorMessage)
: errorMessage;
setErrors((prev) => ({ ...prev, email: translatedMessage }));
createToast({ body: translatedMessage });
} 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.startsWith("auth")
? t(error.details[0].message)
: error.details[0].message
: t("Error validating data."),
});
} else {
const action = await dispatch(login(form));
if (action.payload.success) {
navigate("/uptime");
createToast({
body: t("welcomeBack"),
});
} 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.contrastText,
fontWeight: 600,
fontSize: 28,
},
/* TODO set font size from theme */
"& p": { fontSize: 14, color: theme.palette.primary.contrastTextSecondary },
"& span": { fontSize: "inherit" },
}}
>
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
px={theme.spacing(12)}
gap={theme.spacing(4)}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>Checkmate</Typography>
</Stack>
<Stack
direction="row"
spacing={2}
alignItems="center"
>
<LanguageSelector />
<ThemeSwitch />
</Stack>
</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.primary.lowContrast,
backgroundColor: theme.palette.primary.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}
/>
{/* Registration link */}
<Box textAlign="center">
<Typography
className="forgot-p"
display="inline-block"
color={theme.palette.primary.main}
>
{t("doNotHaveAccount")}
</Typography>
<Typography
component="span"
color={theme.palette.accent.main}
ml={theme.spacing(2)}
sx={{
cursor: "pointer",
"&:hover": {
color: theme.palette.accent.darker,
},
}}
onClick={() => navigate("/register")}
>
{t("registerHere")}
</Typography>
</Box>
</Stack>
</Stack>
);
};
export default Login;
+167
View File
@@ -0,0 +1,167 @@
// Components
import Stack from "@mui/material/Stack";
import AuthHeader from "../components/AuthHeader";
import Button from "@mui/material/Button";
import TextInput from "../../../Components/Inputs/TextInput";
import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments";
import { loginCredentials } from "../../../Validation/validation";
import TextLink from "../../../Components/TextLink";
import Typography from "@mui/material/Typography";
// Utils
import { login } from "../../../Features/Auth/authSlice";
import { useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
import { useState } from "react";
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
import { createToast } from "../../../Utils/toastUtils";
const Login = () => {
// Local state
const [form, setForm] = useState({
email: "",
password: "",
});
const [errors, setErrors] = useState({
email: "",
password: "",
});
// Hooks
const { t } = useTranslation();
const theme = useTheme();
const dispatch = useDispatch();
const navigate = useNavigate();
// Handlers
const onChange = (e) => {
let { name, value } = e.target;
if (name === "email") {
value = value.toLowerCase();
}
const updatedForm = { ...form, [name]: value };
const { error } = loginCredentials.validate({ [name]: value });
setForm(updatedForm);
setErrors((prev) => ({
...prev,
[name]: error?.details?.[0]?.message || "",
}));
};
const onSubmit = async (e) => {
e.preventDefault();
const toSubmit = { ...form };
const { error } = loginCredentials.validate(toSubmit, { abortEarly: false });
if (error) {
const formErrors = {};
for (const err of error.details) {
formErrors[err.path[0]] = err.message;
}
setErrors(formErrors);
return;
}
const action = await dispatch(login(form));
if (action.payload.success) {
navigate("/uptime");
createToast({
body: t("auth.login.toasts.success"),
});
} else {
if (action.payload) {
if (action.payload.msg === "Incorrect password")
setErrors({
password: t("auth.login.errors.password.incorrect"),
});
// dispatch errors
createToast({
body: t("auth.login.toasts.incorrectPassword"),
});
} else {
// unknown errors
createToast({
body: t("common.toasts.unknownError"),
});
}
}
};
return (
<Stack
gap={theme.spacing(10)}
minHeight="100vh"
>
<AuthHeader />
<Stack
margin="auto"
width="100%"
alignItems="center"
gap={theme.spacing(10)}
>
<Typography variant="h1">{t("auth.login.heading")}</Typography>
<Stack
component="form"
width="100%"
maxWidth={600}
alignSelf="center"
justifyContent="center"
borderRadius={theme.spacing(5)}
borderColor={theme.palette.primary.lowContrast}
backgroundColor={theme.palette.primary.main}
padding={theme.spacing(12)}
gap={theme.spacing(12)}
onSubmit={onSubmit}
>
<TextInput
type="email"
name="email"
label={t("auth.common.inputs.email.label")}
isRequired={true}
placeholder={t("auth.common.inputs.email.placeholder")}
autoComplete="email"
value={form.email}
onChange={onChange}
error={errors.email ? true : false}
helperText={errors.email ? t(errors.email) : ""} // Localization keys are in validation.js
/>
<TextInput
type="password"
name="password"
label={t("auth.common.inputs.password.label")}
isRequired={true}
placeholder="••••••••••"
autoComplete="current-password"
value={form.password}
onChange={onChange}
error={errors.password ? true : false}
helperText={errors.password ? t(errors.password) : ""} // Localization keys are in validation.js
endAdornment={<PasswordEndAdornment />}
/>
<Button
variant="contained"
color="accent"
type="submit"
sx={{ width: "30%", alignSelf: "flex-end" }}
>
Login
</Button>
</Stack>
<TextLink
text={t("auth.login.links.forgotPassword")}
linkText={t("auth.login.links.forgotPasswordLink")}
href="/forgot-password"
/>
<TextLink
text={t("auth.login.links.register")}
linkText={t("auth.login.links.registerLink")}
href="/register"
/>
</Stack>
</Stack>
);
};
export default Login;
+24 -18
View File
@@ -1,14 +1,13 @@
import { Box, Button, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router";
import { useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
import { clearAuthState } from "../../Features/Auth/authSlice";
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
import Background from "../../assets/Images/background-grid.svg?react";
import ConfirmIcon from "../../assets/icons/check-outlined.svg?react";
import Logo from "../../assets/icons/checkmate-icon.svg?react";
import IconBox from "../../Components/IconBox";
import { useTranslation } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import "./index.css";
const NewPasswordConfirmed = () => {
@@ -19,7 +18,6 @@ const NewPasswordConfirmed = () => {
const handleNavigate = () => {
dispatch(clearAuthState());
dispatch(clearUptimeMonitorState());
navigate("/login");
};
@@ -54,7 +52,7 @@ const NewPasswordConfirmed = () => {
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>Checkmate</Typography>
<Typography sx={{ userSelect: "none" }}>{t("common.appName")}</Typography>
</Stack>
<Stack
width="100%"
@@ -96,11 +94,13 @@ const NewPasswordConfirmed = () => {
svgHeight={24}
mb={theme.spacing(4)}
>
<ConfirmIcon alt="password confirm icon" />
<ConfirmIcon alt={t("auth.forgotPassword.imageAlts.passwordConfirm")} />
</IconBox>
</Stack>
<Typography component="h1">{t("passwordreset")}</Typography>
<Typography mb={theme.spacing(2)}>{t("authNewPasswordConfirmed")}</Typography>
<Typography component="h1">{t("auth.forgotPassword.heading")}</Typography>
<Typography mb={theme.spacing(2)}>
{t("auth.forgotPassword.subheadings.stepFour")}
</Typography>
</Box>
<Button
variant="contained"
@@ -110,7 +110,7 @@ const NewPasswordConfirmed = () => {
maxWidth: 400,
}}
>
{t("continue")}
{t("auth.common.navigation.continue")}
</Button>
</Stack>
</Stack>
@@ -118,15 +118,21 @@ const NewPasswordConfirmed = () => {
textAlign="center"
p={theme.spacing(12)}
>
<Typography display="inline-block">{t("goBackTo")}</Typography>
<Typography
component="span"
color={theme.palette.accent.main}
ml={theme.spacing(2)}
onClick={handleNavigate}
sx={{ userSelect: "none" }}
>
{t("authLoginTitle")}
<Typography display="inline-block">
<Trans
i18nKey="auth.forgotPassword.links.login"
components={{
a: (
<Typography
component="span"
color={theme.palette.accent.main}
ml={theme.spacing(2)}
onClick={handleNavigate}
sx={{ userSelect: "none" }}
/>
),
}}
/>
</Typography>
</Box>
</Stack>
-399
View File
@@ -1,399 +0,0 @@
import { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useDispatch } from "react-redux";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Button, Stack, Typography } from "@mui/material";
import { StepOne } from "./StepOne";
import { StepTwo } from "./StepTwo";
import { StepThree } from "./StepThree";
import { networkService } from "../../../main";
import { credentials } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import { register } from "../../../Features/Auth/authSlice";
import Background from "../../../assets/Images/background-grid.svg?react";
import Logo from "../../../assets/icons/checkmate-icon.svg?react";
import Mail from "../../../assets/icons/mail.svg?react";
import "../index.css";
import { useTranslation, Trans } from "react-i18next";
/**
* Displays the initial landing page.
*
* @param {Object} props
* @param {boolean} props.isSuperAdmin - Whether the user is creating and admin account
* @param {Function} props.onContinue - Callback function to handle "Continue with Email" button click.
* @returns {JSX.Element}
*/
const LandingPage = ({ isSuperAdmin, onSignup }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<>
<Stack
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
alignItems="center"
textAlign="center"
>
<Box>
<Typography component="h1">{t("signUp")}</Typography>
<Typography>
{isSuperAdmin
? t("authRegisterCreateSuperAdminAccount")
: t("authRegisterCreateAccount")}
</Typography>
</Box>
<Box width="100%">
<Button
variant="outlined"
color="info"
onClick={onSignup}
sx={{
width: "100%",
"& svg": {
mr: theme.spacing(4),
"& path": {
stroke: theme.palette.primary.contrastTextTertiary,
},
},
"&:focus-visible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
},
}}
>
<Mail />
{t("authRegisterSignUpWithEmail")}
</Button>
</Box>
<Box maxWidth={400}>
<Typography className="tos-p">
<Trans
i18nKey="authRegisterBySigningUp"
components={{
a1: (
<Typography
component="span"
sx={{
cursor: "pointer",
"&:hover": {
color: theme.palette.primary.contrastTextTertiary,
},
}}
onClick={() => {
window.open(
"https://bluewavelabs.ca/terms-of-service-open-source",
"_blank",
"noreferrer"
);
}}
/>
),
a2: (
<Typography
component="span"
sx={{
cursor: "pointer",
"&:hover": {
color: theme.palette.primary.contrastTextTertiary,
},
}}
onClick={() => {
window.open(
"https://bluewavelabs.ca/privacy-policy-open-source",
"_blank",
"noreferrer"
);
}}
/>
),
}}
/>
</Typography>
</Box>
</Stack>
</>
);
};
LandingPage.propTypes = {
isSuperAdmin: PropTypes.bool,
onSignup: PropTypes.func,
};
const Register = ({ isSuperAdmin }) => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { token } = useParams();
const theme = useTheme();
const { t } = useTranslation();
// TODO If possible, change the IDs of these fields to match the backend
const idMap = {
"register-firstname-input": "firstName",
"register-lastname-input": "lastName",
"register-email-input": "email",
"register-password-input": "password",
"register-confirm-input": "confirm",
};
const [form, setForm] = useState({
firstName: "",
lastName: "",
email: "",
password: "",
confirm: "",
role: [],
teamId: "",
});
const [errors, setErrors] = useState({});
const [step, setStep] = useState(0);
useEffect(() => {
const fetchInvite = async () => {
if (token !== undefined) {
try {
const res = await networkService.verifyInvitationToken(token);
const invite = res.data.data;
const { email } = invite;
setForm({ ...form, email });
} catch (error) {
navigate("/register", { replace: true });
}
}
};
fetchInvite();
}, []);
/**
* Validates the form data against the validation schema.
*
* @param {Object} data - The form data to validate.
* @param {Object} [options] - Optional settings for validation.
* @returns {Object | undefined} - Returns the validation error object if there are validation errors; otherwise, `undefined`.
*/
const validateForm = (data, options = {}) => {
const { error } = credentials.validate(data, {
abortEarly: false,
...options,
});
return error;
};
/**
* Handles validation errors by setting the state with error messages and displaying a toast notification.
*
* @param {Object} error - The validation error object returned from the validation schema.
*/
const handleError = (error) => {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
createToast({ body: error.details[0].message || "Error validating data." });
};
const handleStepOne = async (e) => {
e.preventDefault();
let error = validateForm({
firstName: form.firstName,
lastName: form.lastName,
});
if (error) {
handleError(error);
return;
}
setStep(2);
};
const handleStepTwo = async (e) => {
e.preventDefault();
let error;
error = validateForm({ email: form.email });
if (error) {
handleError(error);
return;
}
setStep(3);
};
// Final step
// Attempts account registration
const handleStepThree = async (e) => {
e.preventDefault();
const { password, confirm } = e.target.elements;
let registerForm = {
...form,
password: password.value,
confirm: confirm.value,
role: isSuperAdmin ? ["superadmin"] : form.role,
inviteToken: token ? token : "", // Add the token to the request for verification
};
let error = validateForm(registerForm, {
context: { password: registerForm.password },
});
if (error) {
handleError(error);
return;
}
delete registerForm.confirm;
const action = await dispatch(register(registerForm));
if (action.payload.success) {
const authToken = action.payload.data;
navigate("/uptime");
createToast({
body: "Welcome! Your account was created successfully.",
});
} else {
if (action.payload) {
createToast({
body: action.payload.msg,
});
} else {
createToast({
body: "Unknown error.",
});
}
}
};
const handleChange = (event) => {
const { value, id } = event.target;
const name = idMap[id];
const lowerCasedValue =
name === idMap["register-email-input"] ? value?.toLowerCase() || value : value;
setForm((prev) => ({
...prev,
[name]: lowerCasedValue,
}));
const { error } = credentials.validate(
{ [name]: lowerCasedValue },
{ abortEarly: false, context: { password: form.password } }
);
setErrors((prev) => {
const prevErrors = { ...prev };
if (error) prevErrors[name] = error.details[0].message;
else delete prevErrors[name];
return prevErrors;
});
};
return (
<Stack
className="register-page auth"
overflow="hidden"
sx={{
"& h1": {
color: theme.palette.primary.contrastText,
fontWeight: 600,
fontSize: 28,
},
/* TODO set fontsize from theme */
"& p": { fontSize: 14, color: theme.palette.primary.contrastTextSecondary },
"& span": { fontSize: "inherit" },
}}
>
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<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" }}>{t("commonAppName")}</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.primary.lowContrast,
backgroundColor: theme.palette.primary.main,
padding: {
xs: theme.spacing(12),
sm: theme.spacing(20),
},
},
}}
>
{step === 0 ? (
<LandingPage
isSuperAdmin={isSuperAdmin}
onSignup={() => setStep(1)}
/>
) : step === 1 ? (
<StepOne
form={form}
errors={errors}
onSubmit={handleStepOne}
onChange={handleChange}
onBack={() => setStep(0)}
/>
) : step === 2 ? (
<StepTwo
form={form}
errors={errors}
onSubmit={handleStepTwo}
onChange={handleChange}
onBack={() => setStep(1)}
/>
) : step === 3 ? (
<StepThree
/* form={form}
errors={errors} */
onSubmit={handleStepThree}
/* onChange={handleChange} */
onBack={() => setStep(2)}
/>
) : (
""
)}
</Stack>
<Box
textAlign="center"
p={theme.spacing(12)}
>
<Typography display="inline-block">
{t("authRegisterAlreadyHaveAccount")}
</Typography>
<Typography
component="span"
ml={theme.spacing(2)}
onClick={() => {
navigate("/login");
}}
sx={{ userSelect: "none", color: theme.palette.accent.main }}
>
{t("authRegisterLoginLink")}
</Typography>
</Box>
</Stack>
);
};
Register.propTypes = {
isSuperAdmin: PropTypes.bool,
};
export default Register;
@@ -1,136 +0,0 @@
import { useEffect, useRef } from "react";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Button, Stack, Typography } from "@mui/material";
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
import TextInput from "../../../../Components/Inputs/TextInput";
import { useTranslation } from "react-i18next";
StepOne.propTypes = {
form: PropTypes.object,
errors: PropTypes.object,
onSubmit: PropTypes.func,
onChange: PropTypes.func,
onBack: PropTypes.func,
};
/**
* Renders the first step of the sign up process.
*
* @param {Object} props
* @param {Object} props.form - Form state object.
* @param {Object} props.errors - Object containing form validation errors.
* @param {Function} props.onSubmit - Callback function to handle form submission.
* @param {Function} props.onChange - Callback function to handle form input changes.
* @param {Function} props.onBack - Callback function to handle "Back" button click.
* @returns {JSX.Element}
*/
function StepOne({ form, errors, onSubmit, onChange, onBack }) {
const theme = useTheme();
const inputRef = useRef(null);
const { t } = useTranslation();
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
return (
<>
{/* TODO this stack should be a component */}
<Stack
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
textAlign="center"
>
<Box>
<Typography component="h1">{t("signUp")}</Typography>
<Typography>{t("authRegisterStepOnePersonalDetails")}</Typography>
</Box>
<Box
textAlign="left"
component="form"
noValidate
spellCheck={false}
onSubmit={onSubmit}
display="grid"
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
>
<Box
display="grid"
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
>
<TextInput
id="register-firstname-input"
label={t("authRegisterFirstName")}
isRequired={true}
placeholder="Jordan"
autoComplete="given-name"
value={form.firstName}
onChange={onChange}
error={errors.firstName ? true : false}
helperText={errors.firstName}
ref={inputRef}
/>
<TextInput
id="register-lastname-input"
label={t("authRegisterLastName")}
isRequired={true}
placeholder="Ellis"
autoComplete="family-name"
value={form.lastName}
onChange={onChange}
error={errors.lastName ? true : false}
helperText={errors.lastName}
/>
</Box>
<Stack
direction="row"
justifyContent="space-between"
>
{/* TODO buttons should be a component should be a component */}
<Button
variant="outlined"
color="info"
onClick={onBack}
sx={{
px: theme.spacing(5),
"& svg.MuiSvgIcon-root": {
mr: theme.spacing(3),
},
"&:focus-visible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
},
}}
>
<ArrowBackRoundedIcon />
{t("commonBack")}
</Button>
<Button
variant="contained"
color="accent"
type="submit"
disabled={(errors.firstName || errors.lastName) && true}
sx={{
width: "30%",
"&.Mui-focusVisible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
boxShadow: `none`,
},
}}
>
{t("continue")}
</Button>
</Stack>
</Box>
</Stack>
</>
);
}
export { StepOne };
@@ -1,173 +0,0 @@
import { useEffect, useRef } from "react";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Button, Stack, Typography } from "@mui/material";
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
import TextInput from "../../../../Components/Inputs/TextInput";
import Check from "../../../../Components/Check/Check";
import { useValidatePassword } from "../../hooks/useValidatePassword";
import { useTranslation } from "react-i18next";
StepThree.propTypes = {
onSubmit: PropTypes.func,
onBack: PropTypes.func,
};
/**
* Renders the third step of the sign up process.
*
* @param {Object} props
* @param {Function} props.onSubmit - Callback function to handle form submission.
* @param {Function} props.onBack - Callback function to handle "Back" button click.
* @returns {JSX.Element}
*/
function StepThree({ onSubmit, onBack }) {
const theme = useTheme();
const inputRef = useRef(null);
const { t } = useTranslation();
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
const { handleChange, feedbacks, form, errors } = useValidatePassword();
return (
<>
<Stack
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
textAlign="center"
>
<Box>
<Typography component="h1">{t("signUp")}</Typography>
<Typography>{t("createPassword")}</Typography>
</Box>
<Box
component="form"
noValidate
spellCheck={false}
onSubmit={onSubmit}
textAlign="left"
display="grid"
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
sx={{
"& .input-error": {
display: "none",
},
}}
>
<Box
display="grid"
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
>
<TextInput
type="password"
id="register-password-input"
name="password"
label={t("authLoginEnterPassword")}
isRequired={true}
placeholder={t("createAPassword")}
autoComplete="current-password"
value={form.password}
onChange={handleChange}
error={errors.password && errors.password[0] ? true : false}
ref={inputRef}
/>
<TextInput
type="password"
id="register-confirm-input"
name="confirm"
label={t("authSetNewPasswordConfirmPassword")}
isRequired={true}
placeholder={t("confirmPassword")}
autoComplete="current-password"
value={form.confirm}
onChange={handleChange}
error={errors.confirm && errors.confirm[0] ? true : false}
/>
</Box>
<Stack
gap={theme.spacing(4)}
mb={{ xs: theme.spacing(6), sm: theme.spacing(8) }}
>
<Check
noHighlightText={t("authPasswordMustBeAtLeast")}
text={t("authPasswordCharactersLong")}
variant={feedbacks.length}
/>
<Check
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordSpecialCharacter")}
variant={feedbacks.special}
/>
<Check
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordOneNumber")}
variant={feedbacks.number}
/>
<Check
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordUpperCharacter")}
variant={feedbacks.uppercase}
/>
<Check
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordLowerCharacter")}
variant={feedbacks.lowercase}
/>
<Check
noHighlightText={t("authPasswordConfirmAndPassword")}
text={t("authPasswordMustMatch")}
variant={feedbacks.confirm}
/>
</Stack>
<Stack
direction="row"
justifyContent="space-between"
>
<Button
variant="outlined"
color="info"
onClick={onBack}
sx={{
px: theme.spacing(5),
"& svg.MuiSvgIcon-root": {
mr: theme.spacing(3),
},
":focus-visible": {
outline: `2px solid ${theme.palette.primary.lowContrast}`,
outlineOffset: "4px",
},
}}
>
<ArrowBackRoundedIcon />
{t("commonBack")}
</Button>
<Button
type="submit"
variant="contained"
color="accent"
disabled={
form.password.length === 0 ||
form.confirm.length === 0 ||
Object.keys(errors).length !== 0
}
sx={{
width: "30%",
"&.Mui-focusVisible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
boxShadow: `none`,
},
}}
>
{t("continue")}
</Button>
</Stack>
</Box>
</Stack>
</>
);
}
export { StepThree };
@@ -1,126 +0,0 @@
import { useEffect, useRef } from "react";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Button, Stack, Typography } from "@mui/material";
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
import TextInput from "../../../../Components/Inputs/TextInput";
import { useTranslation } from "react-i18next";
StepTwo.propTypes = {
form: PropTypes.object,
errors: PropTypes.object,
onSubmit: PropTypes.func,
onChange: PropTypes.func,
onBack: PropTypes.func,
};
/**
* Renders the second step of the sign up process.
*
* @param {Object} props
* @param {Object} props.form - Form state object.
* @param {Object} props.errors - Object containing form validation errors.
* @param {Function} props.onSubmit - Callback function to handle form submission.
* @param {Function} props.onChange - Callback function to handle form input changes.
* @param {Function} props.onBack - Callback function to handle "Back" button click.
* @returns {JSX.Element}
*/
function StepTwo({ form, errors, onSubmit, onChange, onBack }) {
const theme = useTheme();
const inputRef = useRef(null);
const { t } = useTranslation();
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
return (
<>
<Stack
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
textAlign="center"
>
<Box>
<Typography component="h1">{t("signUp")}</Typography>
<Typography>{t("enterEmail")}</Typography>
</Box>
<Box
component="form"
textAlign="left"
noValidate
spellCheck={false}
onSubmit={onSubmit}
mb={theme.spacing(5)}
display="grid"
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
>
<TextInput
type="email"
id="register-email-input"
label={t("authRegisterEmail")}
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 &&
(errors.email.includes("required")
? t("authRegisterEmailRequired")
: errors.email.includes("valid email")
? t("authRegisterEmailInvalid")
: t(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 />
{t("commonBack")}
</Button>
<Button
variant="contained"
color="accent"
onClick={onSubmit}
disabled={errors.email && true}
sx={{
width: "30%",
"&.Mui-focusVisible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
boxShadow: `none`,
},
}}
>
{t("continue")}
</Button>
</Stack>
</Box>
</Stack>
</>
);
}
export { StepTwo };
+353
View File
@@ -0,0 +1,353 @@
// Components
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import AuthHeader from "../components/AuthHeader";
import TextInput from "../../../Components/Inputs/TextInput";
import Check from "../../../Components/Check/Check";
import Button from "@mui/material/Button";
// Utils
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { useParams } from "react-router-dom";
import { networkService } from "../../../main";
import { newOrChangedCredentials } from "../../../Validation/validation";
import { register } from "../../../Features/Auth/authSlice";
import { createToast } from "../../../Utils/toastUtils";
import PropTypes from "prop-types";
const getFeedbackStatus = (form, errors, field, criteria) => {
const fieldErrors = errors?.[field];
const isFieldEmpty = form?.[field]?.length === 0;
const hasError = fieldErrors?.includes(criteria) || fieldErrors?.includes("empty");
const isCorrect = !isFieldEmpty && !hasError;
if (isCorrect) {
return "success";
} else if (hasError) {
return "error";
} else {
return "info";
}
};
const Register = ({ superAdminExists }) => {
// Redux
const { isLoading } = useSelector((state) => state.auth);
// Local state
const [form, setForm] = useState({
firstName: "",
lastName: "",
email: "",
password: "",
confirm: "",
role: [],
teamId: "",
});
const [errors, setErrors] = useState({});
const [feedback, setFeedback] = useState({});
// Hooks
const theme = useTheme();
const { t } = useTranslation();
const { token } = useParams();
const navigate = useNavigate();
const dispatch = useDispatch();
// Effects
useEffect(() => {
const fetchInvite = async () => {
if (token !== undefined) {
try {
const res = await networkService.verifyInvitationToken(token);
const invite = res.data.data;
const { email } = invite;
setForm((prevForm) => {
if (!prevForm.email) {
return { ...prevForm, email };
}
return prevForm;
});
} catch (error) {
navigate("/register", { replace: true });
}
}
};
fetchInvite();
}, [form, token, navigate]);
// Handlers
const onChange = (e) => {
let { name, value } = e.target;
if (name === "email") value = value.toLowerCase();
const updatedForm = { ...form, [name]: value };
const { error } = newOrChangedCredentials.validate(
{ [name]: value },
{ abortEarly: false, context: { password: form.password } }
);
setErrors((prev) => ({
...prev,
[name]: error?.details?.[0]?.message || "",
}));
setForm(updatedForm);
};
const onPasswordChange = (e) => {
const { name, value } = e.target;
const updatedForm = { ...form, [name]: value };
setForm(updatedForm);
const validateValue = { [name]: value };
const validateOptions = { abortEarly: false, context: { password: form.password } };
if (name === "password" && form.confirm.length > 0) {
validateValue.confirm = form.confirm;
validateOptions.context = { password: value };
} else if (name === "confirm") {
validateValue.password = form.password;
}
const { error } = newOrChangedCredentials.validate(validateValue, validateOptions);
const pwdErrors = error?.details.map((error) => ({
path: error.path[0],
type: error.type,
}));
const errorsByPath =
pwdErrors &&
pwdErrors.reduce((acc, { path, type }) => {
if (!acc[path]) {
acc[path] = [];
}
acc[path].push(type);
return acc;
}, {});
const oldErrors = { ...errors };
if (name === "password") {
oldErrors.password = undefined;
} else if (name === "confirm") {
oldErrors.confirm = undefined;
}
const newErrors = { ...oldErrors, ...errorsByPath };
setErrors(newErrors);
const newFeedback = {
length: getFeedbackStatus(updatedForm, errorsByPath, "password", "string.min"),
special: getFeedbackStatus(updatedForm, errorsByPath, "password", "special"),
number: getFeedbackStatus(updatedForm, errorsByPath, "password", "number"),
uppercase: getFeedbackStatus(updatedForm, errorsByPath, "password", "uppercase"),
lowercase: getFeedbackStatus(updatedForm, errorsByPath, "password", "lowercase"),
confirm: getFeedbackStatus(updatedForm, errorsByPath, "confirm", "different"),
};
setFeedback(newFeedback);
};
const onSubmit = async (e) => {
e.preventDefault();
const toSubmit = {
...form,
role: superAdminExists ? form.role : ["superadmin"],
inviteToken: token ? token : "",
};
const { error } = newOrChangedCredentials.validate(toSubmit, {
abortEarly: false,
context: { password: form.password },
});
if (error) {
const formErrors = {};
for (const err of error.details) {
formErrors[err.path[0]] = err.message;
}
setErrors(formErrors);
return;
}
delete toSubmit.confirm;
const action = await dispatch(register(toSubmit));
if (action.payload.success) {
navigate("/uptime");
createToast({
body: t("auth.registration.toasts.success"),
});
} else {
if (action.payload) {
createToast({
body: action.payload.msg,
});
} else {
createToast({
body: t("common.toasts.unknownError"),
});
}
}
};
return (
<Stack
gap={theme.spacing(10)}
minHeight="100vh"
>
<AuthHeader />
<Stack
margin="auto"
width="100%"
alignItems="center"
gap={theme.spacing(10)}
>
<Typography variant="h1">{t("auth.registration.heading.user")}</Typography>
<Stack
component="form"
width="100%"
maxWidth={600}
alignSelf="center"
justifyContent="center"
border={1}
borderRadius={theme.spacing(5)}
borderColor={theme.palette.primary.lowContrast}
backgroundColor={theme.palette.primary.main}
padding={theme.spacing(12)}
gap={theme.spacing(12)}
onSubmit={onSubmit}
>
<Typography variant="h1">
{superAdminExists
? t("auth.registration.heading.user")
: t("auth.registration.heading.superAdmin")}
</Typography>
<Typography>
{superAdminExists
? t("auth.registration.description.user")
: t("auth.registration.description.superAdmin")}
</Typography>
<TextInput
type="email"
name="email"
label={t("auth.common.inputs.email.label")}
isRequired={true}
placeholder={t("auth.common.inputs.email.placeholder")}
autoComplete="email"
value={form.email}
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
onChange={onChange}
error={errors.email ? true : false}
helperText={errors.email ? t(errors.email) : ""} // Localization keys are in validation.js
/>
<TextInput
name="firstName"
label={t("auth.common.inputs.firstName.label")}
isRequired={true}
placeholder={t("auth.common.inputs.firstName.placeholder")}
autoComplete="given-name"
value={form.firstName}
onChange={onChange}
error={errors.firstName ? true : false}
helperText={errors.firstName ? t(errors.firstName) : ""}
/>
<TextInput
name="lastName"
label={t("auth.common.inputs.lastName.label")}
isRequired={true}
placeholder={t("auth.common.inputs.lastName.placeholder")}
autoComplete="family-name"
value={form.lastName}
onChange={onChange}
error={errors.lastName ? true : false}
helperText={errors.lastName ? t(errors.lastName) : ""} // Localization keys are in validation.js
/>
<TextInput
type="password"
id="register-password-input"
name="password"
label={t("auth.common.inputs.password.label")}
isRequired={true}
placeholder="••••••••••"
autoComplete="current-password"
value={form.password}
onChange={onPasswordChange}
error={errors.password && errors.password[0] ? true : false}
helperText={
errors.password === "auth.common.inputs.password.errors.empty"
? t(errors.password)
: ""
} // Other errors are related to required password conditions and are visualized below the input
/>
<TextInput
type="password"
id="register-confirm-input"
name="confirm"
label={t("auth.common.inputs.passwordConfirm.label")}
isRequired={true}
placeholder={t("auth.common.inputs.passwordConfirm.placeholder")}
autoComplete="current-password"
value={form.confirm}
onChange={onPasswordChange}
error={errors.confirm && errors.confirm[0] ? true : false}
/>
<Stack
gap={theme.spacing(4)}
mb={{ xs: theme.spacing(6), sm: theme.spacing(8) }}
>
<Check
noHighlightText={t("auth.common.inputs.password.rules.length.beginning")}
text={t("auth.common.inputs.password.rules.length.highlighted")}
variant={feedback.length}
/>
<Check
noHighlightText={t("auth.common.inputs.password.rules.special.beginning")}
text={t("auth.common.inputs.password.rules.special.highlighted")}
variant={feedback.special}
/>
<Check
noHighlightText={t("auth.common.inputs.password.rules.number.beginning")}
text={t("auth.common.inputs.password.rules.number.highlighted")}
variant={feedback.number}
/>
<Check
noHighlightText={t("auth.common.inputs.password.rules.uppercase.beginning")}
text={t("auth.common.inputs.password.rules.uppercase.highlighted")}
variant={feedback.uppercase}
/>
<Check
noHighlightText={t("auth.common.inputs.password.rules.lowercase.beginning")}
text={t("auth.common.inputs.password.rules.lowercase.highlighted")}
variant={feedback.lowercase}
/>
<Check
noHighlightText={t("auth.common.inputs.password.rules.match.beginning")}
text={t("auth.common.inputs.password.rules.match.highlighted")}
variant={feedback.confirm}
/>
</Stack>
<Button
disabled={isLoading}
variant="contained"
color="accent"
type="submit"
sx={{ width: "30%", alignSelf: "flex-end" }}
>
{t("auth.common.navigation.continue")}
</Button>
</Stack>
</Stack>
</Stack>
);
};
Register.propTypes = {
superAdminExists: PropTypes.bool,
};
export default Register;
+53 -39
View File
@@ -6,7 +6,7 @@ import { useTheme } from "@emotion/react";
import { Box, Stack, Typography, Button } from "@mui/material";
import { setNewPassword } from "../../Features/Auth/authSlice";
import { createToast } from "../../Utils/toastUtils";
import { credentials } from "../../Validation/validation";
import { newOrChangedCredentials } from "../../Validation/validation";
import Check from "../../Components/Check/Check";
import TextInput from "../../Components/Inputs/TextInput";
import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
@@ -16,7 +16,7 @@ import Logo from "../../assets/icons/checkmate-icon.svg?react";
import Background from "../../assets/Images/background-grid.svg?react";
import "./index.css";
import { useValidatePassword } from "./hooks/useValidatePassword";
import { useTranslation } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
const SetNewPassword = () => {
const navigate = useNavigate();
@@ -34,7 +34,7 @@ const SetNewPassword = () => {
const handleSubmit = async (e) => {
e.preventDefault();
const { error } = credentials.validate(form, {
const { error } = newOrChangedCredentials.validate(form, {
abortEarly: false,
context: { password: form.password },
});
@@ -43,20 +43,20 @@ const SetNewPassword = () => {
createToast({
body:
error.details && error.details.length > 0
? error.details[0].message
: "Error validating data.",
? error.details[0].message // FIXME: Potential untranslated string
: t("auth.common.errors.validation"),
});
} else {
const action = await dispatch(setNewPassword({ token, form }));
if (action.payload.success) {
navigate("/new-password-confirmed");
createToast({
body: "Your password was reset successfully.",
body: t("auth.forgotPassword.toasts.success"),
});
} else {
const errorMessage = action.payload
? action.payload.msg
: "Unable to reset password. Please try again later or contact support.";
? action.payload.msg // FIXME: Potential untranslated string
: t("auth.forgotPassword.toasts.error");
createToast({
body: errorMessage,
});
@@ -98,7 +98,7 @@ const SetNewPassword = () => {
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>{t("commonAppName")}</Typography>
<Typography sx={{ userSelect: "none" }}>{t("common.appName")}</Typography>
</Stack>
<Stack
width="100%"
@@ -140,11 +140,11 @@ const SetNewPassword = () => {
svgHeight={24}
mb={theme.spacing(4)}
>
<LockIcon alt="lock icon" />
<LockIcon alt={t("auth.forgotPassword.imageAlts.lock")} />
</IconBox>
</Stack>
<Typography component="h1">{t("authSetNewPasswordTitle")}</Typography>
<Typography>{t("authSetNewPasswordDescription")}</Typography>
<Typography component="h1">{t("auth.forgotPassword.heading")}</Typography>
<Typography>{t("auth.forgotPassword.subheadings.stepThree")}</Typography>
</Box>
<Box
width="100%"
@@ -165,13 +165,17 @@ const SetNewPassword = () => {
id={passwordId}
type="password"
name="password"
label={t("commonPassword")}
label={t("auth.common.inputs.password.label")}
isRequired={true}
placeholder="••••••••"
value={form.password}
onChange={handleChange}
error={errors.password ? true : false}
helperText={errors.password}
helperText={
errors.password === "auth.common.inputs.password.errors.empty"
? t(errors.password)
: ""
} // Other errors are related to required password conditions and are visualized below the input
endAdornment={<PasswordEndAdornment />}
/>
</Box>
@@ -185,13 +189,13 @@ const SetNewPassword = () => {
id={confirmPasswordId}
type="password"
name="confirm"
label={t("authSetNewPasswordConfirmPassword")}
label={t("auth.common.inputs.passwordConfirm.label")}
isRequired={true}
placeholder="••••••••"
placeholder={t("auth.common.inputs.passwordConfirm.placeholder")}
value={form.confirm}
onChange={handleChange}
error={errors.confirm ? true : false}
helperText={errors.confirm}
helperText={t(errors.confirm)} // Localization keys are in validation.js
endAdornment={<PasswordEndAdornment />}
/>
</Box>
@@ -200,33 +204,37 @@ const SetNewPassword = () => {
mb={theme.spacing(12)}
>
<Check
noHighlightText={t("authPasswordMustBeAtLeast")}
text={t("authPasswordCharactersLong")}
noHighlightText={t("auth.common.inputs.password.rules.length.beginning")}
text={t("auth.common.inputs.password.rules.length.highlighted")}
variant={feedbacks.length}
/>
<Check
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordSpecialCharacter")}
noHighlightText={t("auth.common.inputs.password.rules.special.beginning")}
text={t("auth.common.inputs.password.rules.special.highlighted")}
variant={feedbacks.special}
/>
<Check
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordOneNumber")}
noHighlightText={t("auth.common.inputs.password.rules.number.beginning")}
text={t("auth.common.inputs.password.rules.number.highlighted")}
variant={feedbacks.number}
/>
<Check
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordUpperCharacter")}
noHighlightText={t(
"auth.common.inputs.password.rules.uppercase.beginning"
)}
text={t("auth.common.inputs.password.rules.uppercase.highlighted")}
variant={feedbacks.uppercase}
/>
<Check
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordLowerCharacter")}
noHighlightText={t(
"auth.common.inputs.password.rules.lowercase.beginning"
)}
text={t("auth.common.inputs.password.rules.lowercase.highlighted")}
variant={feedbacks.lowercase}
/>
<Check
noHighlightText={t("authPasswordConfirmAndPassword")}
text={t("authPasswordMustMatch")}
noHighlightText={t("auth.common.inputs.password.rules.match.beginning")}
text={t("auth.common.inputs.password.rules.match.highlighted")}
variant={feedbacks.confirm}
/>
</Stack>
@@ -243,7 +251,7 @@ const SetNewPassword = () => {
}
sx={{ width: "100%", maxWidth: 400 }}
>
{t("authSetNewPasswordResetPassword")}
{t("auth.forgotPassword.buttons.resetPassword")}
</Button>
</Stack>
</Stack>
@@ -251,15 +259,21 @@ const SetNewPassword = () => {
textAlign="center"
p={theme.spacing(12)}
>
<Typography display="inline-block">{t("goBackTo")} —</Typography>
<Typography
component="span"
color={theme.palette.primary.main}
ml={theme.spacing(2)}
onClick={() => navigate("/login")}
sx={{ userSelect: "none" }}
>
{t("authLoginTitle")}
<Typography display="inline-block">
<Trans
i18nKey="auth.forgotPassword.links.login"
components={{
a: (
<Typography
component="span"
color={theme.palette.primary.main}
ml={theme.spacing(2)}
onClick={() => navigate("/login")}
sx={{ userSelect: "none" }}
/>
),
}}
/>
</Typography>
</Box>
</Stack>
@@ -0,0 +1,45 @@
// Components
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Logo from "../../../assets/icons/checkmate-icon.svg?react";
import LanguageSelector from "../../../Components/LanguageSelector";
import ThemeSwitch from "../../../Components/ThemeSwitch";
// Utils
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
const AuthHeader = () => {
// Hooks
const theme = useTheme();
const { t } = useTranslation();
return (
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
px={theme.spacing(12)}
gap={theme.spacing(4)}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>{t("common.appName")}</Typography>
</Stack>
<Stack
direction="row"
spacing={theme.spacing(2)}
alignItems="center"
>
<LanguageSelector />
<ThemeSwitch />
</Stack>
</Stack>
);
};
export default AuthHeader;
@@ -1,5 +1,5 @@
import { useMemo, useState } from "react";
import { credentials } from "../../../Validation/validation";
import { newOrChangedCredentials } from "../../../Validation/validation";
const getFeedbackStatus = (form, errors, field, criteria) => {
const fieldErrors = errors[field];
@@ -36,7 +36,7 @@ function useValidatePassword() {
validateValue.password = form.password;
}
const { error } = credentials.validate(validateValue, validateOptions);
const { error } = newOrChangedCredentials.validate(validateValue, validateOptions);
const errors = error?.details.map((error) => ({
path: error.path[0],
type: error.type,
@@ -1,31 +0,0 @@
import { useState } from "react";
import { networkService } from "../../../../main";
import { useSelector } from "react-redux";
import { createToast } from "../../../../Utils/toastUtils";
const useCreateDistributedUptimeMonitor = ({ isCreate, monitorId }) => {
const [isLoading, setIsLoading] = useState(false);
const [networkError, setNetworkError] = useState(false);
const createDistributedUptimeMonitor = async ({ form }) => {
setIsLoading(true);
try {
if (isCreate) {
await networkService.createMonitor({ monitor: form });
} else {
await networkService.updateMonitor({ monitor: form, monitorId });
}
return true;
} catch (error) {
setNetworkError(true);
createToast({ body: error?.response?.data?.msg ?? error.message });
return false;
} finally {
setIsLoading(false);
}
};
return [createDistributedUptimeMonitor, isLoading, networkError];
};
export { useCreateDistributedUptimeMonitor };
@@ -1,31 +0,0 @@
import { useEffect, useState } from "react";
import { networkService } from "../../../../main";
import { createToast } from "../../../../Utils/toastUtils";
export const useMonitorFetch = ({ monitorId, isCreate }) => {
const [networkError, setNetworkError] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [monitor, setMonitor] = useState(undefined);
useEffect(() => {
const fetchMonitors = async () => {
try {
if (isCreate) return;
const res = await networkService.getUptimeDetailsById({
monitorId: monitorId,
normalize: true,
});
setMonitor(res?.data?.data ?? {});
} catch (error) {
setNetworkError(true);
createToast({ body: error.message });
} finally {
setIsLoading(false);
}
};
fetchMonitors();
}, [monitorId, isCreate]);
return [monitor, isLoading, networkError];
};
export default useMonitorFetch;
@@ -1,349 +0,0 @@
// Components
import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import ConfigBox from "../../../Components/ConfigBox";
import TextInput from "../../../Components/Inputs/TextInput";
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
import Radio from "../../../Components/Inputs/Radio";
import Checkbox from "../../../Components/Inputs/Checkbox";
import Select from "../../../Components/Inputs/Select";
import { createToast } from "../../../Utils/toastUtils";
// Utility
import { useTheme } from "@emotion/react";
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { monitorValidation } from "../../../Validation/validation";
import { useParams } from "react-router-dom";
import { useCreateDistributedUptimeMonitor } from "./Hooks/useCreateDistributedUptimeMonitor";
import { useMonitorFetch } from "./Hooks/useMonitorFetch";
import { useTranslation } from "react-i18next";
// Constants
const MS_PER_MINUTE = 60000;
const SELECT_VALUES = [
{ _id: 1, name: "1 minute" },
{ _id: 2, name: "2 minutes" },
{ _id: 3, name: "3 minutes" },
{ _id: 4, name: "4 minutes" },
{ _id: 5, name: "5 minutes" },
];
const parseUrl = (url) => {
try {
return new URL(url);
} catch (error) {
return null;
}
};
const CreateDistributedUptime = () => {
const { monitorId } = useParams();
const isCreate = typeof monitorId === "undefined";
const { t } = useTranslation();
let BREADCRUMBS = [
{ name: `distributed uptime`, path: "/distributed-uptime" },
...(isCreate ? [] : [{ name: "details", path: `/distributed-uptime/${monitorId}` }]),
{ name: isCreate ? "create" : "configure", path: `` },
];
// Redux state
const { user } = useSelector((state) => state.auth);
// Local state
const [https, setHttps] = useState(true);
const [notifications, setNotifications] = useState([]);
const [form, setForm] = useState({
type: "distributed_http",
name: "",
url: "",
interval: 1,
});
const [errors, setErrors] = useState({});
//utils
const theme = useTheme();
const navigate = useNavigate();
const [createDistributedUptimeMonitor, isLoading, networkError] =
useCreateDistributedUptimeMonitor({ isCreate, monitorId });
const [monitor, monitorIsLoading, monitorNetworkError] = useMonitorFetch({
monitorId,
isCreate,
});
// Effect to set monitor to fetched monitor
useEffect(() => {
if (typeof monitor !== "undefined") {
const parsedUrl = parseUrl(monitor?.url);
const protocol = parsedUrl?.protocol?.replace(":", "") || "";
setHttps(protocol === "https");
const newForm = {
name: monitor.name,
interval: monitor.interval / MS_PER_MINUTE,
url: parsedUrl.host,
type: monitor.type,
};
setForm(newForm);
}
}, [monitor]);
// Handlers
const handleCreateMonitor = async () => {
const monitorToSubmit = { ...form };
// Prepend protocol to url
monitorToSubmit.url = `http${https ? "s" : ""}://` + monitorToSubmit.url;
const { error } = monitorValidation.validate(monitorToSubmit, {
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;
}
// Append needed fields
monitorToSubmit.description = form.name;
monitorToSubmit.interval = form.interval * MS_PER_MINUTE;
monitorToSubmit.teamId = user.teamId;
monitorToSubmit.userId = user._id;
monitorToSubmit.notifications = notifications;
const success = await createDistributedUptimeMonitor({ form: monitorToSubmit });
if (success) {
createToast({ body: "Monitor created successfully!" });
navigate("/distributed-uptime");
} else {
createToast({ body: "Failed to create monitor." });
}
};
const handleChange = (event) => {
let { name, value } = event.target;
setForm({
...form,
[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 currentNotifications = [...notifications];
const notificationAlreadyExists = notifications.some((notification) => {
if (notification.type === type && notification.address === value) {
return true;
}
return false;
});
if (notificationAlreadyExists) {
currentNotifications = currentNotifications.filter((notification) => {
if (notification.type === type && notification.address === value) {
return false;
}
return true;
});
} else {
currentNotifications.push({ type, address: value });
}
setNotifications(currentNotifications);
};
return (
<Box>
<Breadcrumbs list={BREADCRUMBS} />
<Stack
component="form"
gap={theme.spacing(12)}
mt={theme.spacing(6)}
onSubmit={() => console.log("submit")}
>
<Typography
component="h1"
variant="h1"
>
<Typography
component="span"
fontSize="inherit"
>
{isCreate ? "Create your" : "Edit your"}{" "}
</Typography>
<Typography
component="span"
variant="h2"
fontSize="inherit"
fontWeight="inherit"
>
{t("monitor")}
</Typography>
</Typography>
<ConfigBox>
<Box>
<Typography component="h2">{t("settingsGeneralSettings")}</Typography>
<Typography component="p">{t("distributedUptimeCreateSelectURL")}</Typography>
</Box>
<Stack gap={theme.spacing(15)}>
<TextInput
type={"url"}
id="monitor-url"
startAdornment={<HttpAdornment https={https} />}
label="URL to monitor"
https={https}
placeholder={"www.google.com"}
disabled={!isCreate}
value={form.url}
name="url"
onChange={handleChange}
error={errors["url"] ? true : false}
helperText={errors["url"]}
/>
<TextInput
type="text"
id="monitor-name"
label="Display name"
isOptional={true}
placeholder={"Google"}
value={form.name}
name="name"
onChange={handleChange}
error={errors["name"] ? true : false}
helperText={errors["name"]}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">{t("distributedUptimeCreateChecks")}</Typography>
<Typography component="p">
{t("distributedUptimeCreateChecksDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(12)}>
<Stack gap={theme.spacing(6)}>
<Radio
id="monitor-checks-http"
title="Website monitoring"
desc="Use HTTP(s) to monitor your website or API endpoint."
size="small"
value="http"
name="type"
checked={true}
onChange={handleChange}
/>
{form.type === "http" || form.type === "distributed_http" ? (
<ButtonGroup
disabled={!isCreate}
sx={{ ml: theme.spacing(16) }}
>
<Button
variant="group"
filled={https.toString()}
onClick={() => setHttps(true)}
>
{t("https")}
</Button>
<Button
variant="group"
filled={(!https).toString()}
onClick={() => setHttps(false)}
>
{t("http")}
</Button>
</ButtonGroup>
) : (
""
)}
</Stack>
{errors["type"] ? (
<Box className="error-container">
<Typography
component="p"
className="input-error"
color={theme.palette.error.contrastText}
>
{errors["type"]}
</Typography>
</Box>
) : (
""
)}
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">
{t("distributedUptimeCreateIncidentNotification")}
</Typography>
<Typography component="p">
{t("distributedUptimeCreateIncidentDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(6)}>
<Checkbox
id="notify-email-default"
label={`Notify via email (to ${user.email})`}
isChecked={notifications.some(
(notification) => notification.type === "email"
)}
value={user?.email}
onChange={(event) => handleNotifications(event, "email")}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">
{t("distributedUptimeCreateAdvancedSettings")}
</Typography>
</Box>
<Stack gap={theme.spacing(12)}>
<Select
id="monitor-interval"
label="Check frequency"
name="interval"
value={form.interval}
onChange={handleChange}
items={SELECT_VALUES}
/>
</Stack>
</ConfigBox>
<Stack
direction="row"
justifyContent="flex-end"
>
<Button
variant="contained"
color="accent"
onClick={() => handleCreateMonitor()}
disabled={!Object.values(errors).every((value) => value === undefined)}
loading={isLoading}
>
{isCreate ? "Create monitor" : "Configure monitor"}
</Button>
</Stack>
</Stack>
</Box>
);
};
export default CreateDistributedUptime;
@@ -1,50 +0,0 @@
// Components
import { Stack, Typography } from "@mui/material";
import { ColContainer } from "../../../../../Components/StandardContainer";
import SmartToyIcon from "@mui/icons-material/SmartToy";
import Dot from "../../../../../Components/Dot";
// Utils
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
const MESSAGES = [
"I've checked the network status, and we're seeing excellent performance across all regions.",
"The network is stable and functioning optimally. All connections are active and stable.",
"I've reviewed the network status, and everything looks great. No issues detected.",
"The network is up and running smoothly. All connections are active and stable.",
"I've checked the network status, and everything is looking good. No issues detected.",
];
const ChatBot = ({ sx }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<ColContainer
backgroundColor={theme.palette.chatbot.background}
sx={{ ...sx }}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(4)}
>
<SmartToyIcon sx={{ color: theme.palette.chatbot.textAccent }} />
<Typography color={theme.palette.chatbot.textAccent}>Status Bot</Typography>
<Dot
color={theme.palette.chatbot.textAccent}
style={{ opacity: 0.4 }}
/>
<Typography
variant="body2"
color={theme.palette.chatbot.textAccent}
sx={{ opacity: 0.4 }}
>
{t("now")}
</Typography>
</Stack>
<Typography>{MESSAGES[Math.floor(Math.random() * MESSAGES.length)]}</Typography>
</ColContainer>
);
};
export default ChatBot;
@@ -1,92 +0,0 @@
// Components
import { Box, Stack, Button } from "@mui/material";
import Image from "../../../../../Components/Image";
import SettingsIcon from "../../../../../assets/icons/settings-bold.svg?react";
//Utils
import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router-dom";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
const Controls = ({ isDeleteOpen, setIsDeleteOpen, isDeleting, monitorId }) => {
const theme = useTheme();
const navigate = useNavigate();
const { t } = useTranslation();
return (
<Stack
direction="row"
gap={theme.spacing(2)}
>
<Box>
<Button
variant="contained"
color="error"
onClick={() => setIsDeleteOpen(!isDeleteOpen)}
loading={isDeleting}
>
{t("delete")}
</Button>
</Box>
<Box>
<Button
variant="contained"
color="secondary"
onClick={() => {
navigate(`/distributed-uptime/configure/${monitorId}`);
}}
sx={{
px: theme.spacing(5),
"& svg": {
mr: theme.spacing(3),
"& path": {
stroke: theme.palette.secondary.contrastText,
},
},
}}
>
<SettingsIcon /> {t("configure")}
</Button>
</Box>
</Stack>
);
};
Controls.propTypes = {
isDeleting: PropTypes.bool,
monitorId: PropTypes.string,
isDeleteOpen: PropTypes.bool.isRequired,
setIsDeleteOpen: PropTypes.func.isRequired,
};
const ControlsHeader = ({ isDeleting, isDeleteOpen, setIsDeleteOpen, monitorId }) => {
const theme = useTheme();
return (
<Stack
alignSelf="flex-start"
direction="row"
width="100%"
gap={theme.spacing(2)}
justifyContent="flex-end"
alignItems="flex-end"
>
<Controls
isDeleting={isDeleting}
isDeleteOpen={isDeleteOpen}
setIsDeleteOpen={setIsDeleteOpen}
monitorId={monitorId}
/>
</Stack>
);
};
ControlsHeader.propTypes = {
monitorId: PropTypes.string,
isDeleting: PropTypes.bool,
isDeleteOpen: PropTypes.bool.isRequired,
setIsDeleteOpen: PropTypes.func.isRequired,
};
export default ControlsHeader;
@@ -1,98 +0,0 @@
// Components
import { Stack, Typography } from "@mui/material";
import PulseDot from "../../../../../Components/Animated/PulseDot";
import "flag-icons/css/flag-icons.min.css";
import { ColContainer } from "../../../../../Components/StandardContainer";
// Utils
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
const DeviceTicker = ({ data, width = "100%", connectionStatus }) => {
const theme = useTheme();
const { t } = useTranslation();
const statusColor = {
up: theme.palette.success.main,
down: theme.palette.error.main,
undefined: theme.palette.warning.main,
};
return (
<ColContainer sx={{ height: "100%" }}>
<Stack
direction="row"
gap={theme.spacing(4)}
>
<PulseDot color={statusColor[connectionStatus]} />
<Typography
variant="h1"
mb={theme.spacing(8)}
sx={{ alignSelf: "center" }}
>
{connectionStatus === "up" ? "Connected" : "Connecting..."}
</Typography>
</Stack>
<div
style={{
overflowX: "auto",
maxWidth: "100%",
// Optional: add a max height if you want vertical scrolling too
// maxHeight: '400px',
// overflowY: 'auto'
}}
>
<table style={{ width: "100%" }}>
<thead>
<tr>
<th style={{ textAlign: "left", paddingLeft: theme.spacing(4) }}>
<Typography>{t("country")}</Typography>
</th>
<th style={{ textAlign: "left", paddingLeft: theme.spacing(4) }}>
<Typography>{t("city")}</Typography>
</th>
<th style={{ textAlign: "right", paddingLeft: theme.spacing(4) }}>
<Typography>{t("response")}</Typography>
</th>
<th style={{ textAlign: "right", paddingLeft: theme.spacing(4) }}>
<Typography sx={{ whiteSpace: "nowrap" }}>{"UPT BURNED"}</Typography>
</th>
</tr>
</thead>
<tbody>
{data.map((dataPoint) => {
const countryCode = dataPoint?.countryCode?.toLowerCase() ?? null;
const flag = countryCode ? `fi fi-${countryCode}` : null;
const city = dataPoint?.city !== "" ? dataPoint?.city : "Unknown";
return (
<tr key={Math.random()}>
<td style={{ paddingLeft: theme.spacing(4) }}>
<Typography>
{flag ? <span className={flag} /> : null}{" "}
{countryCode?.toUpperCase() ?? "N/A"}
</Typography>
</td>
<td style={{ paddingLeft: theme.spacing(4) }}>
<Typography>{city}</Typography>
</td>
<td style={{ textAlign: "right", paddingLeft: theme.spacing(4) }}>
<Typography>
{Math.floor(dataPoint.responseTime)} {t("ms")}
</Typography>
</td>
<td style={{ textAlign: "right", paddingLeft: theme.spacing(4) }}>
<Typography color={theme.palette.warning.main}>
+{dataPoint.uptBurnt}
</Typography>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</ColContainer>
);
};
export default DeviceTicker;
@@ -1,169 +0,0 @@
{
"id": "43f36e14-e3f5-43c1-84c0-50a9c80dc5c7",
"name": "MapLibre",
"zoom": 0.861983335785597,
"pitch": 0,
"center": [17.6543171043124, 32.9541203267468],
"glyphs": "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf",
"layers": [
{
"id": "background",
"type": "background",
"paint": {
"background-color": "#121217"
},
"filter": ["all"],
"layout": {
"visibility": "visible"
},
"maxzoom": 24
},
{
"id": "coastline",
"type": "line",
"paint": {
"line-blur": 0.5,
"line-color": "#000000",
"line-width": {
"stops": [
[0, 2],
[6, 6],
[14, 9],
[22, 18]
]
}
},
"filter": ["all"],
"layout": {
"line-cap": "round",
"line-join": "round",
"visibility": "visible"
},
"source": "maplibre",
"maxzoom": 24,
"minzoom": 0,
"source-layer": "countries"
},
{
"id": "countries-fill",
"type": "fill",
"paint": {
"fill-color": "#292929"
},
"filter": ["all"],
"layout": {
"visibility": "visible"
},
"source": "maplibre",
"maxzoom": 24,
"source-layer": "countries"
},
{
"id": "countries-boundary",
"type": "line",
"paint": {
"line-color": "#484848",
"line-width": {
"stops": [
[1, 1],
[6, 2],
[14, 6],
[22, 12]
]
},
"line-opacity": {
"stops": [
[3, 0.5],
[6, 1]
]
}
},
"layout": {
"line-cap": "round",
"line-join": "round",
"visibility": "visible"
},
"source": "maplibre",
"maxzoom": 24,
"source-layer": "countries"
},
{
"id": "countries-label",
"type": "symbol",
"paint": {
"text-color": "rgba(8, 37, 77, 1)",
"text-halo-blur": {
"stops": [
[2, 0.2],
[6, 0]
]
},
"text-halo-color": "rgba(255, 255, 255, 1)",
"text-halo-width": {
"stops": [
[2, 1],
[6, 1.6]
]
}
},
"filter": ["all"],
"layout": {
"text-font": ["Open Sans Semibold"],
"text-size": {
"stops": [
[2, 10],
[4, 12],
[6, 16]
]
},
"text-field": {
"stops": [
[2, "{ABBREV}"],
[4, "{NAME}"]
]
},
"visibility": "visible",
"text-max-width": 10,
"text-transform": {
"stops": [
[0, "uppercase"],
[2, "none"]
]
}
},
"source": "maplibre",
"maxzoom": 24,
"minzoom": 2,
"source-layer": "centroids"
},
{
"id": "data-dots",
"type": "circle",
"source": "data-dots",
"paint": {
"circle-radius": 3,
"circle-color": ["get", "color"],
"circle-opacity": 0.5
}
}
],
"bearing": 0,
"sources": {
"maplibre": {
"url": "https://demotiles.maplibre.org/tiles/tiles.json",
"type": "vector"
},
"data-dots": {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": []
}
}
},
"version": 8,
"metadata": {
"maptiler:copyright": "This style was generated on MapTiler Cloud. Usage is governed by the license terms in https://github.com/maplibre/demotiles/blob/gh-pages/LICENSE",
"openmaptiles:version": "3.x"
}
}
@@ -1,33 +0,0 @@
import baseStyle from "./DistributedUptimeMapStyle.json";
const buildStyle = (theme, mode) => {
const style = JSON.parse(JSON.stringify(baseStyle));
if (mode === "dark") {
return baseStyle;
}
if (style.layers) {
const newLayers = style.layers.map((layer) => {
if (layer.id === "background") {
layer.paint["background-color"] = theme.palette.map.main;
}
if (layer.id === "countries-fill") {
layer.paint["fill-color"] = theme.palette.map.lowContrast;
}
if (layer.id === "coastline") {
layer.paint["line-color"] = theme.palette.map.highContrast;
}
if (layer.id === "countries-boundary") {
layer.paint["line-color"] = theme.palette.map.highContrast;
}
return layer;
});
style.layers = newLayers;
}
return style;
};
export default buildStyle;
@@ -1,118 +0,0 @@
import "maplibre-gl/dist/maplibre-gl.css";
import PropTypes from "prop-types";
import { useRef, useState, useEffect } from "react";
import { useTheme } from "@mui/material/styles";
import maplibregl from "maplibre-gl";
import { useSelector } from "react-redux";
import buildStyle from "./buildStyle";
const DistributedUptimeMap = ({
width = "100%",
checks,
height,
minHeight = "350px",
}) => {
const mapContainer = useRef(null);
const map = useRef(null);
const theme = useTheme();
const [mapLoaded, setMapLoaded] = useState(false);
const mode = useSelector((state) => state.ui.mode);
const initialTheme = useRef(theme);
const initialMode = useRef(mode);
const colorLookup = (avgResponseTime) => {
if (avgResponseTime <= 150) {
return "#00FF00"; // Green
} else if (avgResponseTime <= 250) {
return "#FFFF00"; // Yellow
} else {
return "#FF0000"; // Red
}
};
useEffect(() => {
if (mapContainer.current && !map.current) {
const initialStyle = buildStyle(initialTheme.current, initialMode.current);
map.current = new maplibregl.Map({
container: mapContainer.current,
style: initialStyle,
center: [0, 20],
zoom: 0.8,
attributionControl: false,
canvasContextAttributes: {
antialias: true,
preserveDrawingBuffer: true,
},
});
}
map.current.on("load", () => {
setMapLoaded(true);
});
return () => {
if (map.current) {
map.current.remove();
map.current = null;
}
};
}, []);
useEffect(() => {
const style = buildStyle(theme, mode);
if (map.current && mapLoaded) {
map.current.setStyle(style);
}
}, [theme, mode, mapLoaded]);
useEffect(() => {
if (map.current && checks?.length > 0) {
// Convert dots to GeoJSON
const geojson = {
type: "FeatureCollection",
features: checks.map((check) => {
return {
type: "Feature",
geometry: {
type: "Point",
coordinates: [check._id.lng, check._id.lat],
},
properties: {
color: theme.palette.accent.main,
// color: colorLookup(check.avgResponseTime) || "blue", // Default to blue if no color specified
},
};
}),
};
// Update the source with new dots
const source = map.current.getSource("data-dots");
if (source) {
source.setData(geojson);
}
}
}, [checks, theme, mapLoaded]);
return (
<div
ref={mapContainer}
style={{
width: width,
height: height,
minHeight: minHeight,
borderRadius: theme.spacing(4),
borderColor: theme.palette.primary.lowContrast,
borderStyle: "solid",
borderWidth: 1,
}}
/>
);
};
DistributedUptimeMap.propTypes = {
checks: PropTypes.array,
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
minHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
export default DistributedUptimeMap;
Loaded 100 of 301 files, more files were not shown because too many files have changed in this diff. Show more