Compare commits

...
614 Commits
Author SHA1 Message Date
Alexander Holliday a924227805 Merge pull request #2964 from bluewave-labs/develop
develop -> master
2025-09-18 13:40:45 -07:00
Alexander Holliday e4e75632c5 Merge pull request #2963 from bluewave-labs/translation-update-78
🌐 Updated Translations from POEditor
2025-09-18 13:40:10 -07:00
GitHub Action ddb7c24832 feat: translations updated from POEditor 2025-09-18 20:25:11 +00:00
Alexander Holliday 2144d3f8c0 Merge pull request #2962 from bluewave-labs/develop
develop -> master
2025-09-18 12:04:51 -07:00
Alexander Holliday 4a824dec81 Merge pull request #2960 from bluewave-labs/feat/bump-version
bump version
2025-09-18 12:03:26 -07:00
Alex Holliday b35fee1be8 bump version 2025-09-18 12:02:49 -07:00
Alexander Holliday 1b167af12b Merge pull request #2959 from Owaiseimdad/Addition-of-sliding-window-implementation
Addition of incident tab in creation and config page which is same.
2025-09-18 11:50:55 -07:00
Owaise f2962e34ae Fixed the formatting. 2025-09-19 00:06:31 +05:30
Owaise 386db5029a Addition of incident tab in creation and config page which is same. 2025-09-19 00:03:18 +05:30
Alexander Holliday 0e6e54a124 Merge pull request #2957 from FrancocDev/develop
Allow jsonPath to be used along expectedValue and validate the response.
2025-09-18 09:47:53 -07:00
Franco Carrara c666cb3879 fix formatting issues 2025-09-17 23:46:20 -03:00
Franco Carrara 4039517683 Add response messages to string service 2025-09-17 23:33:45 -03:00
Franco Carrara 00fc7b8038 Allow jsonPath to be used along expectedValue and validate the response. 2025-09-17 23:23:16 -03:00
Alexander Holliday 81d283ec8d Merge pull request #2956 from ajhollid/bump-dependencies
chore: bump deps
2025-09-17 15:17:14 -07:00
Alex Holliday 105100e349 bump deps 2025-09-17 15:07:55 -07:00
Alexander Holliday 4598690dbf Merge pull request #2954 from brownyroll/develop
feat: Support Thailand language
2025-09-16 09:00:51 -07:00
Bamz Kittisak Udomsri f68341c315 feat: thailang 2025-09-16 20:41:13 +07:00
Alexander Holliday e8a8cea909 Merge pull request #2953 from bluewave-labs/fix-skip-maintenance-notifications
Fix: Skip notifications during maintenance windows
2025-09-15 11:13:28 -07:00
karenvicent bf53f107e3 Merge branch 'develop' into fix-skip-maintenance-notifications 2025-09-15 12:29:47 -04:00
karenvicent 00de974528 Fix: Skip notifications for monitors during active maintenance windows 2025-09-15 12:28:37 -04:00
Alexander Holliday 0445398e94 Merge pull request #2952 from bluewave-labs/feat-notifications-body
Add monitor name and URL formatting for hardware notifications
2025-09-15 09:15:10 -07:00
karenvicent 272e2ba955 add monitor name and url to infrastructure alert 2025-09-15 08:34:51 -04:00
Alexander Holliday 0a2adc2d92 Merge pull request #2948 from bluewave-labs/feat-discord-format
Add Discord embed message formatting for notifications
2025-09-12 09:03:19 -07:00
karenvicent 7008fc1770 Merge branch 'develop' into feat-discord-format 2025-09-12 10:14:07 -04:00
karenvicent 041a261540 add llama sugestions 2025-09-12 10:13:38 -04:00
Gorkem Cetin (BWL) 1008d819f7 Update README.md 2025-09-11 22:40:11 -04:00
karenvicent fb84a79a04 Add Discord embed message formatting for notifications 2025-09-11 16:29:08 -04:00
Alexander Holliday fcd6786149 Merge pull request #2936 from bluewave-labs/develop
develop -> master
2025-09-08 20:43:21 -07:00
Alexander Holliday 45bfdbdce0 Merge pull request #2935 from bluewave-labs/fix/mono-dist-ping
fix vite.config
2025-09-08 20:40:33 -07:00
Alex Holliday 1537c2141f fix vite.config 2025-09-08 20:39:45 -07:00
Alexander Holliday ea968ebf3c Merge pull request #2933 from bluewave-labs/develop
develop -> master
2025-09-08 20:33:35 -07:00
Alexander Holliday 3ba1a52644 Merge pull request #2932 from bluewave-labs/fix/mono-dist-ping
fix: mono dist ping
2025-09-08 20:31:01 -07:00
Alex Holliday a015cc6706 fix dist-arm, bump version 2025-09-08 20:29:38 -07:00
Alex Holliday 5bad362793 fix server build order 2025-09-08 20:27:20 -07:00
Alexander Holliday 5520756edd Merge pull request #2923 from bluewave-labs/feat-page-initialization-hook
Add PageStateWrapper and fix initial render flash
2025-09-08 09:12:14 -07:00
karenvicent ba954a1d6e run format 2025-09-08 11:15:14 -04:00
karenvicent 2944ff298c Merge branch 'develop' into feat-page-initialization-hook 2025-09-08 10:50:15 -04:00
karenvicent 80d121e87a add llama suggestions 2025-09-08 10:49:42 -04:00
Alexander Holliday 8a7b852974 Merge pull request #2929 from bluewave-labs/develop
develop -> master
2025-09-08 07:24:16 -07:00
Alexander Holliday ed8352d857 Merge pull request #2928 from bluewave-labs/fix/install-ping
fix: install ping
2025-09-08 07:11:34 -07:00
Alex Holliday 41f64ca9fe bump version 2025-09-08 07:10:29 -07:00
Alex Holliday 738e49dc92 Merge branch 'develop' into fix/install-ping 2025-09-08 07:10:15 -07:00
Alex Holliday 68a16257c6 add ping to dist 2025-09-08 07:09:39 -07:00
Alex Holliday 5dcbae55f4 install ping 2025-09-08 06:57:10 -07:00
Alexander Holliday e0d963256c Merge pull request #2926 from bluewave-labs/fix/bump-version
fix: bump version
2025-09-08 06:45:15 -07:00
Alexander Holliday 2bff4bbd15 Merge pull request #2927 from bluewave-labs/fix/ping-error-logging
fix: throw ping error if present
2025-09-08 06:45:00 -07:00
Alex Holliday a0eba89f34 bump version 2025-09-08 06:43:35 -07:00
Alex Holliday 4e78de9da2 throw error if present 2025-09-08 06:42:36 -07:00
karenvicent 85f00eb7e8 Merge branch 'develop' into feat-page-initialization-hook 2025-09-05 15:59:14 -04:00
karenvicent 8e13855b66 feat create pagestate wrapper 2025-09-05 15:53:22 -04:00
Alexander Holliday 06f49e24c2 Merge pull request #2867 from antonstrover/fix/uptime%-charts-toggles
fix(status-page): toggles for charts and uptime %
2025-09-05 09:08:40 -07:00
antonstrover e598ae45f7 Merge branch 'develop' into fix/uptime%-charts-toggles 2025-09-05 16:46:04 +01:00
anton.strover ad7f469e06 add display name for bar 2025-09-05 16:27:48 +01:00
Alexander Holliday c8d3e3b70c Merge pull request #2921 from bluewave-labs/develop
develop -> master
2025-09-04 15:46:30 -07:00
Alexander Holliday cb57ff5cc2 Merge pull request #2920 from bluewave-labs/fix/arm
fix/arm
2025-09-04 15:44:44 -07:00
Alex Holliday 72f070fbcb fix multiarch build 2025-09-04 15:15:09 -07:00
Alex Holliday fb0a86d37c update docker compoes and docker file 2025-09-04 13:39:53 -07:00
Alexander Holliday 36402dfefa Merge pull request #2870 from bluewave-labs/feat/add-team-member
Add direct user registration for team management
2025-09-03 09:58:35 -07:00
karenvicent c61874dbdd fix format error 2025-09-03 12:34:35 -04:00
Alex Holliday 20848f28d5 package 2025-09-03 09:21:24 -07:00
Alexander Holliday 5d23966f00 Merge pull request #2919 from bluewave-labs/fix/format-action
update format action, no need for ci
2025-09-03 09:20:43 -07:00
Alex Holliday 294cc66e4a update format action, no need for ci 2025-09-03 09:20:15 -07:00
Alex Holliday 0fd2e7032a update package-lock 2025-09-03 09:17:19 -07:00
Alex Holliday 340b85043e Merge branch 'feat/add-team-member' of github.com:bluewave-labs/checkmate into feat/add-team-member 2025-09-03 09:15:22 -07:00
Alex Holliday 1821ae3c2b add root gitignore 2025-09-03 09:14:30 -07:00
karenvicent c536cb2afa run format 2025-09-03 10:18:09 -04:00
karenvicent 3f79b4c2f6 Merge branch 'develop' into feat/add-team-member 2025-09-03 09:56:31 -04:00
karenvicent 137ba9185e fix details 2025-09-03 09:55:53 -04:00
Alexander Holliday 2f6a0913dc Merge pull request #2917 from bluewave-labs/fix/bump-scheduler
bump scheduler
2025-09-02 10:35:09 -07:00
Alex Holliday 2c7336d440 bump scheduler 2025-09-02 10:34:33 -07:00
Gorkem Cetin (BWL) 5a022ff3f6 Create .coderabbit.yml so it doesnt add summary to the PR 2025-09-01 19:33:33 -04:00
Alexander Holliday fe4855500e Merge pull request #2896 from PaiJi/feat/update-i18n-scripts
feat(i18n): add zh-CN
2025-09-01 10:46:21 -07:00
Alexander Holliday ce8f5cbe0c Merge pull request #2878 from antonstrover/fix/failed-prop-type-status
Fixed Bar component
2025-09-01 10:43:43 -07:00
Alexander Holliday 7a90a6f9f4 Merge pull request #2915 from bluewave-labs/develop
develop->master
2025-09-01 10:33:09 -07:00
Alexander Holliday b6bebe33fe Merge branch 'master' into develop 2025-09-01 10:32:59 -07:00
Alexander Holliday 9cf8072c1e Merge pull request #2914 from bluewave-labs/fix/bump-multiarch-node
Fix/bump multiarch node
2025-09-01 10:30:51 -07:00
Alex Holliday 0ca00ca6a1 esbuild 2025-09-01 10:28:41 -07:00
Alex Holliday 619c5242db bump versions 2025-09-01 10:14:57 -07:00
Alexander Holliday ff07854590 Merge pull request #2913 from bluewave-labs/revert-2910-fix/alpine-to-slim
Revert "pin dependency"
2025-09-01 09:27:03 -07:00
Alexander Holliday ba99989c66 Revert "pin dependency" 2025-09-01 09:26:51 -07:00
Alexander Holliday 3c75933cf2 Merge pull request #2912 from bluewave-labs/revert-2911-develop
Revert "develop->master"
2025-09-01 09:26:35 -07:00
Alexander Holliday ea9ef7afd3 Revert "develop->master" 2025-09-01 09:26:23 -07:00
Alexander Holliday f5e633a781 Merge pull request #2911 from bluewave-labs/develop
develop->master
2025-09-01 09:23:01 -07:00
Alexander Holliday 90b94816dd Merge pull request #2910 from bluewave-labs/fix/alpine-to-slim
pin dependency
2025-09-01 09:22:26 -07:00
Alex Holliday 11dcaef3ff pin 2025-09-01 09:21:56 -07:00
Alex Holliday 3f3e7f71ec pin dependency 2025-09-01 09:21:11 -07:00
Alexander Holliday b6070572cf Merge pull request #2909 from bluewave-labs/develop
develop->master
2025-09-01 09:15:48 -07:00
Alexander Holliday e818a02a2a Merge pull request #2908 from bluewave-labs/fix/alpine-to-slim
fix build
2025-09-01 09:15:17 -07:00
Alex Holliday d8c8e222c8 fix build 2025-09-01 09:14:35 -07:00
Alexander Holliday f12b9715bc Merge pull request #2907 from bluewave-labs/develop
develop -> master
2025-09-01 09:07:57 -07:00
Alexander Holliday 31c65d3e0b Merge pull request #2906 from bluewave-labs/fix/alpine-to-slim
add omit optional flag
2025-09-01 09:07:21 -07:00
Alex Holliday fcd6741cf9 add omit optional flag 2025-09-01 09:06:42 -07:00
Alexander Holliday d663b9230f Merge pull request #2905 from bluewave-labs/develop
Develop -> Master
2025-09-01 09:00:25 -07:00
Alexander Holliday 5f2d61ad7a Merge pull request #2904 from bluewave-labs/fix/alpine-to-slim
remove alpine fix
2025-09-01 08:58:42 -07:00
Alex Holliday ec80a2dc0b remove alpine fix 2025-09-01 08:57:55 -07:00
Alexander Holliday 2cece3de59 Merge pull request #2902 from bluewave-labs/develop
develop -> master
2025-09-01 08:54:38 -07:00
Alexander Holliday 79eb580802 Merge pull request #2901 from bluewave-labs/fix/alpine-to-slim
alpine -> slim
2025-09-01 08:54:10 -07:00
Alex Holliday b93e3327a7 alpine -> slim 2025-09-01 08:53:03 -07:00
anton.strover f01030a47a move width out of sx object 2025-09-01 11:44:10 +01:00
JiPai 3cbd907c09 feat(i18n): add zh-CN 2025-09-01 04:53:24 +08:00
karenvicent 8fae69b7a4 Merge branch 'develop' into feat/add-team-member 2025-08-31 10:19:01 -04:00
karenvicent e45dd4240e fix errors 2025-08-31 10:18:15 -04:00
Alexander Holliday 16a8552e6c Merge pull request #2890 from bluewave-labs/develop
develop->master
2025-08-30 19:31:37 -07:00
Alexander Holliday c953ec6152 Merge pull request #2889 from bluewave-labs/fix/package-lock
regenerate package-lock
2025-08-30 19:31:02 -07:00
Alex Holliday a433f10240 regenerate package-lock 2025-08-30 19:30:24 -07:00
Alexander Holliday b73b97b5e9 Merge pull request #2887 from bluewave-labs/feat/add-maintenance-window-description
Update maintenance window description
2025-08-30 19:01:45 -07:00
gorkem-bwl 0607e0b228 refactor: simplify maintenance window description per feedback
- Remove redundant words 'active' and 'completely'
- Remove technical details about specific check types
- Keep the description concise while maintaining clarity
- Maintain all key information about maintenance behavior
2025-08-30 21:57:17 -04:00
gorkem-bwl 3bd4f67ec7 docs: add comprehensive maintenance window behavior description
- Update maintenance window description to clearly explain what happens during maintenance
- Clarify that ALL monitoring types are suspended (pings, HTTP, port checks, etc.)
- Explain that no notifications or alerts will be triggered
- Describe status page behavior (maintenance indicator shown)
- Note that monitoring resumes automatically when maintenance ends
- Clarify that maintenance periods don't affect uptime calculations
- Provide users with complete understanding of maintenance window impact
2025-08-30 21:31:44 -04:00
Alexander Holliday 845d5a5d51 Merge pull request #2885 from bluewave-labs/develop
develop->master
2025-08-30 16:23:40 -07:00
Alexander Holliday 0101da731f Merge pull request #2884 from bluewave-labs/fix/bump-version
bump version
2025-08-30 16:23:03 -07:00
Alex Holliday 68592acd39 bump version 2025-08-30 16:22:16 -07:00
Alexander Holliday b86fc5edb3 Merge pull request #2883 from bluewave-labs/develop
develop -> master
2025-08-30 16:21:19 -07:00
Alexander Holliday 32b54ddf11 Merge pull request #2882 from bluewave-labs/feat/migration
add migration for statusWindowThreshold
2025-08-30 16:20:23 -07:00
Alex Holliday f8a0741e6c <= -> < 2025-08-30 16:19:17 -07:00
Alex Holliday 652d231771 add migration for statusWindowThreshold 2025-08-30 16:07:03 -07:00
Alexander Holliday 1c3c5f4fec Merge pull request #2881 from bluewave-labs/fix/queue
fix: queue
2025-08-30 11:30:38 -07:00
Alex Holliday e7006a4a44 fix default value 2025-08-30 11:28:54 -07:00
Alex Holliday 0f81d54f25 remove success toast 2025-08-30 11:18:05 -07:00
Alex Holliday f3121c24c6 add interval to queue table 2025-08-30 11:17:34 -07:00
Alex Holliday 7abd0f2cd4 add root level gitignore 2025-08-30 11:13:53 -07:00
Alex Holliday 46b342005a update monitor interval and data 2025-08-30 11:11:58 -07:00
Alex Holliday 4e0349bd58 bump queue 2025-08-30 11:11:19 -07:00
Alexander Holliday 30a569233b Merge pull request #2873 from bluewave-labs/feat/docker-container-name-monitoring
feat: Enable Docker container monitoring by name in addition to ID
2025-08-29 09:33:45 -07:00
Alex Holliday 44ba6b4d0f early returns 2025-08-29 09:22:02 -07:00
Alexander Holliday 9065070eb2 Merge pull request #2871 from bluewave-labs/feat/incident-tooltip
feat:incident tooltip
2025-08-29 09:07:35 -07:00
anton.strover fd951bfdd5 Fixed Bar component
Moved width prop from direct prop to sx prop, updated PropTypes to accept string|object.
2025-08-29 11:24:57 +01:00
anton.strover 27d613b594 remove carried away-ness 2025-08-29 10:49:22 +01:00
gorkem-bwl ba757b9aa4 style: fix prettier formatting in validation.js 2025-08-29 00:58:03 -04:00
gorkem-bwlandClaude c445463c0c refactor: improve Docker container matching with deterministic priority-based logic
Address code review feedback to eliminate ambiguity issues and improve reliability:

**Backend Improvements:**
- Implement priority-based matching: exact ID → exact name → partial ID
- Add input normalization (strip leading slashes, case-insensitive)
- Add ambiguity detection with warning logs for debugging
- Maintain full backwards compatibility with existing monitors

**Frontend Improvements:**
- Update validation regex to support leading slashes (/container-name)
- Improve error message clarity for user guidance

**Resolves Ambiguity Issues:**
- Container name "abc" vs Container ID "abc123..." now deterministic
- Multiple containers with similar names handled predictably
- Case-insensitive matching for better user experience

**Priority Order:**
1. Exact full ID match (64-char) - highest priority
2. Exact container name match - case-insensitive
3. Partial ID match - backwards compatibility fallback

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-29 00:53:40 -04:00
gorkem-bwlandClaude 4693177e4b feat: add Docker container monitoring by name
- Update Docker monitoring to support both container names and IDs
- Maintain backward compatibility with existing container ID monitoring
- Add logic to find containers by name (with or without "/" prefix)
- Update frontend UI labels and placeholders to reflect name/ID support
- Update instructions to clarify both name and ID are supported

Resolves user request: "It would be great if we could monitor docker containers by name and not just ID - given that the ID changes when containers are taken down for any reason"

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-29 00:27:01 -04:00
Alex Holliday 30b135eba3 add tooltip to incident table 2025-08-28 12:21:34 -07:00
Alex Holliday bda48e07f1 add optional tooltip 2025-08-28 12:21:23 -07:00
karenvicent 7c8ab64e34 fix sucess toast msg 2025-08-28 13:38:05 -04:00
karenvicent ec7855bd02 Merge branch 'develop' into feat/add-team-member 2025-08-28 13:21:20 -04:00
karenvicent d401eefd6b Add direct user registration 2025-08-28 12:58:38 -04:00
Alexander Holliday 0ad759a384 Merge pull request #2865 from bluewave-labs/fix/status-page-deletion-error
Fix status page deletion error
2025-08-27 14:49:45 -07:00
gorkem-bwl d3d20b29c3 Fix status page deletion error - missing module reference
- Fix deleteStatusPage method call to use statusPageModule
- Change this.db.deleteStatusPage to this.db.statusPageModule.deleteStatusPage
- Ensures consistency with other controller methods
- Resolves "this.db.deleteStatusPage is not a function" error

Fixes bug where status page deletion failed due to incorrect database module reference.
2025-08-27 17:47:11 -04:00
Alexander Holliday fbb570c0d5 Merge pull request #2862 from bluewave-labs/fix/advanced-matching
fix: advanced matching
2025-08-26 14:56:20 -07:00
Alex Holliday 2318082bb8 fix advanced matching 2025-08-26 14:54:24 -07:00
Alexander Holliday 05dd5f7de2 Merge pull request #2858 from bluewave-labs/develop
develop -> master
2025-08-25 15:06:19 -07:00
Alexander Holliday 841251d09e Merge pull request #2857 from bluewave-labs/fix/model-types
fix/model types
2025-08-25 15:05:41 -07:00
Alexander Holliday a4dd8f70f0 Merge branch 'develop' into fix/model-types 2025-08-25 15:05:30 -07:00
Alex Holliday 9d0d1329b6 bump version 2025-08-25 15:04:44 -07:00
Alex Holliday 7a929777b4 fix model types 2025-08-25 15:03:31 -07:00
Alexander Holliday ce3ec99c9f Merge pull request #2855 from bluewave-labs/develop
develop -> master
2025-08-25 14:38:06 -07:00
Alexander Holliday 4f7847275e Merge pull request #2854 from bluewave-labs/feat/bump-version
bump version
2025-08-25 14:37:21 -07:00
Alex Holliday 9a5ddb22b3 bump version 2025-08-25 14:37:00 -07:00
Alexander Holliday b1322cbec3 Merge pull request #2853 from bluewave-labs/fix/string-service-path
fix: string service path, resolves #2847, resolves #2848
2025-08-25 14:32:21 -07:00
Alex Holliday 637bc84136 fix string service path 2025-08-25 14:30:41 -07:00
Alexander Holliday 3147f02b6a Merge pull request #2843 from bluewave-labs/fix/notification-channel-validation
fix notification validation
2025-08-22 11:34:38 -07:00
Alex Holliday dabff74946 fix notification validation 2025-08-22 11:33:14 -07:00
Gorkem Cetin (BWL) de99577b9d Merge pull request #2838 from bluewave-labs/disabledbydefault
Fix Settings Page Select Component Value Handling for URL Visibility Option
2025-08-22 10:38:16 -04:00
Alexander Holliday 1846432601 Merge pull request #2839 from bluewave-labs/fix/docker-compose-arm
fix: update arm dist docker compose
2025-08-21 22:39:03 -07:00
Alex Holliday 6f307dc822 update arm dist docker compose 2025-08-21 22:37:59 -07:00
imjainish f43210e29e Address PR Comments - Format 2025-08-21 23:13:50 -04:00
imjainish 50b9be4eca Address PR Comments 2025-08-21 23:05:50 -04:00
imjainish 1a38222974 Fix Disabled Field in Settings 2025-08-21 22:39:41 -04:00
Alexander Holliday cb9cd0a43c Merge pull request #2829 from bluewave-labs/fix-timestamps-format
fix: add missing tooltip date format for 'recent' timeframe in charts
2025-08-21 11:39:13 -07:00
Alexander Holliday c2e2ae8dbd Merge pull request #2834 from bluewave-labs/develop
develop -> master
2025-08-21 10:13:48 -07:00
Alexander Holliday df790a92be Merge pull request #2836 from bluewave-labs/fix/bump-version
bump version
2025-08-21 10:13:13 -07:00
Alex Holliday e1f9242b34 bump version 2025-08-21 10:12:44 -07:00
Alexander Holliday 9d2a52e639 Merge pull request #2833 from bluewave-labs/translation-update-77
🌐 Updated Translations from POEditor
2025-08-21 09:25:38 -07:00
GitHub Action 5cbca9457e feat: translations updated from POEditor 2025-08-21 16:22:53 +00:00
Alexander Holliday 25c66cff43 Merge pull request #2831 from bluewave-labs/develop
develop -> master
2025-08-21 09:15:40 -07:00
karenvicent 75a2e0deee Merge branch 'develop' into fix-timestamps-format 2025-08-20 18:16:50 -04:00
karenvicent b857137f1a fix data format in uptime and infrastructure charts 2025-08-20 18:14:42 -04:00
Alexander Holliday 40141cdcb0 Merge pull request #2828 from bluewave-labs/fix/version-bump
Fix/version bump
2025-08-20 14:10:16 -07:00
Alex Holliday c5c1d6903e 3.1 2025-08-20 14:09:41 -07:00
Alex Holliday f15c8188b9 bump again 2025-08-20 14:07:30 -07:00
Alexander Holliday 8f971cfb02 Merge pull request #2826 from bluewave-labs/develop
develop -> master
2025-08-20 14:05:35 -07:00
Alexander Holliday 32c811ec5a Merge pull request #2827 from bluewave-labs/fix/version-bump
fix: bump version
2025-08-20 14:04:34 -07:00
Alex Holliday 4febc56c30 bump version 2025-08-20 14:04:02 -07:00
Alexander Holliday f22ee4a68b Merge pull request #2824 from bluewave-labs/fix/remove-log
remove console statement
2025-08-20 14:01:57 -07:00
Alex Holliday 87bec0399f remove console statement 2025-08-20 14:01:32 -07:00
Alexander Holliday 1f29255dd1 Merge pull request #2821 from bluewave-labs/refactor/create-maintenance-window
refactor create maintenance window page
2025-08-20 13:29:56 -07:00
karenvicent 4a11638182 refactor create maintenance window page 2025-08-20 12:51:24 -04:00
Alexander Holliday 10743372f8 Merge pull request #2813 from bluewave-labs/feat/n-of-m-frontend
feat/n of m frontend
2025-08-19 08:08:52 -07:00
Alex Holliday d4b11bbb76 extract strings, sort json 2025-08-18 15:46:41 -07:00
Alex Holliday ced2417c98 add incident config 2025-08-18 15:34:30 -07:00
Alex Holliday 27415aeca6 fix undefined bug, updated server validation 2025-08-18 15:34:15 -07:00
Alexander Holliday 9bd2e336a6 Merge pull request #2811 from bluewave-labs/feat/alerts-n-of-m
feat: alerts on  `n` of `m`, resolves #2366
2025-08-18 14:16:35 -07:00
Alex Holliday d5778ec173 update stauts service to use n of m status changes 2025-08-18 13:40:56 -07:00
Alex Holliday 74e80f086a udpate monitor model 2025-08-18 13:40:22 -07:00
Alexander Holliday b1c6191fa1 Merge pull request #2809 from bluewave-labs/fix/fe/minor-changes
Fix: Minor Changes
2025-08-18 12:56:00 -07:00
Alexander Holliday 8e0245c844 Merge pull request #2807 from bluewave-labs/feat/simplify-checks
feat: unify check models
2025-08-18 12:54:45 -07:00
Br0wnHammer 613fbf8c39 Fix: Minor Changes 2025-08-19 01:11:56 +05:30
Alex Holliday 1788cda5c2 let -> const 2025-08-18 11:27:41 -07:00
Alex Holliday 155adf549e add backend changes 2025-08-18 10:55:12 -07:00
Alex Holliday 5463542532 unify check models 2025-08-18 10:52:00 -07:00
Alexander Holliday f5a25bc0e5 Merge pull request #2806 from bluewave-labs/fix/pager_duty
fix: update pager_duty validation, resolves #2805
2025-08-18 09:17:22 -07:00
Alex Holliday 993339d0d1 update pager_duty validation 2025-08-17 20:21:18 -07:00
Alexander Holliday bc4fe897cf Merge pull request #2804 from bluewave-labs/refactor-create-infrastruture
Refactor create infrastruture
2025-08-17 15:16:28 -07:00
Alexander Holliday c773bdaabd Merge pull request #2802 from bluewave-labs/feat/network-service-refactor
feat: network service refactor
2025-08-17 15:08:40 -07:00
Alex Holliday 9f9a2be1d8 fix missing URL 2025-08-17 15:08:12 -07:00
karenvicent 1e4b0c2f20 Merge branch 'develop' into refactor-create-infrastruture 2025-08-17 15:28:49 -04:00
karenvicent 56025f6d56 refactor create infraestructure monitor page 2025-08-17 15:28:06 -04:00
Alexander Holliday 5188b90f32 Merge pull request #2800 from mattstein/fix/pagespeed-validation
Fix: resolve erroneous PageSpeed settings validation
2025-08-15 17:47:30 -07:00
Matt Stein 7178f537a8 Update with changes from code review. 2025-08-15 16:40:21 -07:00
Alex Holliday 45ac589975 refactor network service 2025-08-15 16:04:43 -07:00
Alex Holliday 9925e10ffc add timings to model 2025-08-15 16:03:12 -07:00
Alex Holliday e3fde8d564 add got 2025-08-15 16:02:52 -07:00
Matt Stein 8aee01e008 Fix PageSpeed settings validation. 2025-08-15 14:34:42 -07:00
Alexander Holliday cd203ff067 Merge pull request #2794 from Owaiseimdad/Network-patch-fixes-part-1
Fixed the selector and also null points for networks
2025-08-15 13:12:04 -07:00
Alexander Holliday 254cb6d80f Merge pull request #2788 from bluewave-labs/fix/filter-header-alignment
Fix: Improve vertical alignment of text and plus icons in filter headers
2025-08-15 12:21:50 -07:00
Alexander Holliday 9b3ad54d0a Merge pull request #2797 from bluewave-labs/develop
develop -> master
2025-08-15 12:21:10 -07:00
Alexander Holliday 4ae3bb5780 Merge pull request #2795 from bluewave-labs/hotfix/path
fix path, remove redis form build script
2025-08-15 09:18:18 -07:00
Alex Holliday b8aa1ef765 fix path, remove redis form build script 2025-08-15 09:17:33 -07:00
gorkem-bwl 5e5daef49a Move vertical alignment styling from component to theme level
- Move MuiSelect vertical alignment from FilterHeader component to globalTheme.js
- Ensures consistent vertical alignment for all Select components application-wide
- Removes component-specific styling in favor of theme-level consistency
- Addresses reviewer feedback to make this a theme-level change

Components already with custom Select styling will continue to work as local styles override theme styles.
2025-08-15 03:38:22 -04:00
Owaise 88c478c533 Formatting is done. 2025-08-15 11:37:38 +05:30
Owaise 1ecec115e4 Fixed the selector and also null points for networks. 2025-08-15 11:28:22 +05:30
Alexander Holliday 8ff597eedb Merge pull request #2791 from bluewave-labs/fix/remove-logs-descriptions
Fix: Remove description text and dividers from Logs sections
2025-08-14 20:36:44 -07:00
Alexander Holliday 69f74488e5 Merge pull request #2790 from bluewave-labs/fix/logs-default-tab
Fix: Set Server Logs as default tab on Logs page
2025-08-14 20:34:15 -07:00
gorkem-bwl ded9a27f42 fix: remove excess spacing after removing description text
Removed top margin (mt={theme.spacing(10)}) and adjusted sticky position
from the log level selector since it was creating unwanted space between
the tab bar and the 'Log level' dropdown after removing the description
text and divider.
2025-08-14 22:22:05 -04:00
gorkem-bwl 43284ae625 fix: remove description text and dividers from Logs sections
Removed description strings and divider lines from:
- Server logs: 'System logs - last 1000 lines' and divider
- Queue metrics: queue description and divider

This cleans up the UI by removing redundant descriptive text
that users can infer from the tab names and content context.
2025-08-14 22:19:54 -04:00
gorkem-bwl f7b1ef9741 fix: set Server Logs as default tab on Logs page
Changed default tab from Diagnostics (index 2) to Server Logs (index 0)
when users navigate to the Logs section from the sidebar. This makes
Server Logs the active tab by default, which is more intuitive since
it's the first tab in the list.
2025-08-14 22:11:47 -04:00
gorkem-bwl a6d445818f fix: improve vertical alignment of text and plus icons in filter headers
Added flex alignment styles to MuiSelect-select class to ensure proper vertical
alignment between text labels and plus icons in Type, Status, and State filter
buttons on the Uptime page.
2025-08-14 20:15:56 -04:00
Alexander Holliday 122a84b9e2 Merge pull request #2787 from bluewave-labs/develop
develop -> master for 3.0-beta
2025-08-14 15:28:35 -07:00
Alexander Holliday 065a6cb1fd Merge pull request #2785 from bluewave-labs/fix/caputre
add capture to prod and staging docker-compose
2025-08-14 12:49:11 -07:00
Alex Holliday ae9eb494ae add capture to prod and staging docker-compose 2025-08-14 12:48:49 -07:00
Gorkem Cetin (BWL) 0241797e79 Update README.md 2025-08-14 15:48:11 -04:00
Alexander Holliday 9d45947b08 Merge pull request #2782 from bluewave-labs/fix/version
bump versoin
2025-08-14 11:27:13 -07:00
Alex Holliday f2c26c0eed bump versoin 2025-08-14 11:26:46 -07:00
Alexander Holliday f7a1416070 Merge pull request #2780 from bluewave-labs/hotfix/remove-redis-from-workflow
remove redis from production workflow
2025-08-14 10:32:36 -07:00
Alex Holliday 9e05d024a8 remove redis from production workflow 2025-08-14 10:32:10 -07:00
Alexander Holliday 3bd4c8d747 Merge pull request #2778 from bluewave-labs/hotfix/workflow
add release workflow
2025-08-14 10:00:49 -07:00
Alex Holliday 9ae7314f54 add release workflow 2025-08-14 10:00:20 -07:00
Alexander Holliday c77d1e3322 Merge pull request #2777 from bluewave-labs/hotfix/workflow
remove redis from workflows
2025-08-14 09:59:11 -07:00
Alex Holliday 0e767d6581 remove redis from workflows 2025-08-14 09:58:41 -07:00
Alexander Holliday 7ff5935f94 Merge pull request #2775 from Tafadzwa-Chiwozva/feature/custom-ca-trust-docs
docs: add guide and tooling for custom CA trust in Dockerized Checkmate (Fixes #2747)
2025-08-14 09:52:52 -07:00
Alexander Holliday cb4566f829 Merge pull request #2772 from singh-kanwarpreet/feature/sticky-header
Made NavBar Still
2025-08-14 09:43:31 -07:00
Alexander Holliday 52249d5b7e Merge pull request #2774 from bluewave-labs/fix/remove-mogno-replica-set-config
fix: remove mogno replica set config
2025-08-14 09:35:09 -07:00
Alex Holliday 22e0507fbd fix arm dist health check 2025-08-14 09:34:30 -07:00
Alexander Holliday e32e72de7a Merge pull request #2741 from Owaiseimdad/Network-tab-implementation-FE
Network tab implementation fe
2025-08-14 09:22:09 -07:00
Owaise ff31ee773f Added a drop down so that we have choice to make about which network interface to choose. 2025-08-14 19:35:12 +05:30
singh-kanwarpreet a1cbbf5d42 Added zIndex as earlier causes problem in diagnostics 2025-08-14 15:06:01 +05:30
singh-kanwarpreet ce667bc53b Improved code quality 2025-08-14 15:02:44 +05:30
Alex Holliday d5526b89d9 remove redis references 2025-08-13 15:22:03 -07:00
Alex Holliday 096cfccf40 update dokcer compose files to remove mongo replica set config 2025-08-13 15:16:01 -07:00
Tafadzwa Chiwozva f381b0ce4a docs: add guide and tooling for custom CA trust in Dockerized Checkmate (Fixes #2747) 2025-08-13 17:50:30 -04:00
Alexander Holliday 867b530573 Merge pull request #2765 from bluewave-labs/refactor-login-page
Refactor login page
2025-08-13 14:38:12 -07:00
Owaise 2141c3eacd Metrics validated. 2025-08-14 00:04:53 +05:30
singh-kanwarpreet 13a5f699ba Updated Style 2025-08-13 22:53:18 +05:30
singh-kanwarpreet 7ab5f4ab09 Refactored code 2025-08-13 22:41:13 +05:30
singh-kanwarpreet 480ba034ce Made NavBar Still 2025-08-13 14:47:09 +05:30
karenvicent d560e923d2 Merge branch 'develop' into refactor-login-page 2025-08-12 14:30:55 -04:00
karenvicent 235fc979b2 move hooks to login file 2025-08-12 14:30:11 -04:00
Owaise f5aefb1f0b Ran formattor. 2025-08-12 23:49:45 +05:30
Owaise e27cc8a12b Added delta instead of avg, and formatted the code to re-use already existing code. 2025-08-12 23:48:47 +05:30
Alexander Holliday f2e2d5c2ed Merge pull request #2766 from mertssmnoglu/ci/format-remove-close-pr-step
ci: remove extra 'Close PR' step in check-format workflow
2025-08-12 10:08:23 -07:00
Alexander Holliday 6d999eccbd Merge pull request #2767 from bluewave-labs/fix/reduce-animation-size-in-empty-states
Fix: Reduce animated gif size to 2/3 in empty states
2025-08-12 10:00:03 -07:00
gorkem-bwl 136144b8ec fix: use transform scale instead of width for better border radius preservation 2025-08-12 11:03:46 -04:00
Owaise 355a445ead Fixed the code for linus, windows and mac based. Also made changes to pipeline. 2025-08-12 10:05:58 +05:30
gorkem-bwl c490508252 fix: reduce animated gif size to 2/3 in empty states
Reduces animation size from 100% to 66.67% width for better visual balance
2025-08-11 23:28:35 -04:00
Gorkem Cetin (BWL) 7b4160ac92 Update README.md 2025-08-11 20:38:53 -04:00
karenvicent 781854d85c delete console.log 2025-08-11 18:42:33 -04:00
Mert Şişmanoğlu 86dc71687d ci: remove extra 'Close PR' step in check-format workflow
Signed-off-by: Mert Şişmanoğlu <mertssmnoglu@gmail.com>
2025-08-12 01:41:45 +03:00
karenvicent f0e79121a7 Merge branch 'develop' into refactor-login-page 2025-08-11 18:22:11 -04:00
karenvicent ed1b88f1fd refactor login page 2025-08-11 18:21:23 -04:00
Alexander Holliday bb5a3e73c8 Merge pull request #2763 from bluewave-labs/security/add-jwt-cookie-infrastructure
Add JWT Cookie Infrastructure
2025-08-11 15:09:10 -07:00
Alexander Holliday 38b2025286 Merge branch 'develop' into security/add-jwt-cookie-infrastructure 2025-08-11 15:08:54 -07:00
Alexander Holliday c12f5880e2 Delete server/utils/sanitization.js 2025-08-11 15:07:42 -07:00
Alexander Holliday 94cf22ab8e Merge pull request #2760 from bluewave-labs/security/add-sanitization-infrastructure
Add Input Sanitization Infrastructure
2025-08-11 15:03:37 -07:00
Alex Holliday 0bc8142e71 move to middleware 2025-08-11 15:02:07 -07:00
Alexander Holliday d890b972be Merge pull request #2762 from bluewave-labs/security/apply-input-sanitization
Apply Input Sanitization for XSS Protection
2025-08-11 14:53:32 -07:00
gorkem-bwl 05945a9a74 feat: add JWT cookie infrastructure
Adds the foundation for secure cookie-based authentication without changing
the authentication flow. This prepares the codebase for moving JWT tokens
from Redux state to httpOnly cookies in a follow-up PR.

Changes:
- Added cookie-parser dependency for HTTP cookie handling
- Added cookieParser() middleware to Express application
- Created cookieHelpers.js utilities for consistent cookie options
- Includes getAuthCookieOptions() for setting secure authentication cookies
- Includes getClearAuthCookieOptions() for clearing cookies on logout

Infrastructure only - no behavioral changes to authentication flow yet.

Files added/modified:
- package.json (cookie-parser dependency)
- src/app.js (cookieParser middleware)
- src/utils/cookieHelpers.js (cookie utilities)

Next steps:
- Follow-up PR will modify JWT verification to check cookies
- Enable secure cookie-based authentication
- Add logout functionality to clear httpOnly cookies

Risk level: LOW (infrastructure only, no authentication changes)
2025-08-11 17:47:09 -04:00
gorkem-bwl 88b68a9df0 fix: apply prettier formatting to app.js
Fixed formatting issues to pass CI/CD checks
2025-08-11 17:39:08 -04:00
gorkem-bwl 8b322a0ec0 feat: apply input sanitization to prevent XSS attacks
Enables comprehensive XSS protection by applying sanitization middleware
to all incoming requests. Uses the sanitization utilities added in previous PR.

Changes:
- Added sanitizeBody() and sanitizeQuery() middleware after express.json()
- Enhanced Content Security Policy headers for additional XSS protection
- Added 'object-src: none' and 'base-uri: self' directives
- All user inputs now automatically sanitized before processing

Security improvements:
- Prevents stored XSS attacks through HTML sanitization
- Recursive sanitization handles nested objects and arrays
- Maintains backward compatibility with existing functionality
- Strengthened CSP headers block unsafe content injection

Files changed: src/app.js (9 lines added)
Risk level: Medium (global middleware application)
2025-08-11 17:33:05 -04:00
gorkem-bwl 7968ed8a19 fix: apply prettier formatting to sanitization.js
Fixed formatting issues identified by CI/CD system
2025-08-11 17:26:28 -04:00
gorkem-bwl 2fba22d0f6 feat: add input sanitization infrastructure
Adds foundational utilities for XSS prevention without applying them yet.
This prepares the codebase for comprehensive input sanitization in a follow-up PR.

Changes:
- Added isomorphic-dompurify and jsdom dependencies
- Created sanitization utilities in src/utils/sanitization.js
- Includes recursive object sanitization functions
- Provides Express middleware for body and query sanitization

Infrastructure only - no behavioral changes yet.

Files added:
- src/utils/sanitization.js (sanitization utilities)
- package.json (new dependencies)

Next steps:
- Apply sanitization middleware in follow-up PR
- Enable XSS protection across application
2025-08-11 17:23:14 -04:00
Alexander Holliday cce930b90a Merge pull request #2757 from bluewave-labs/fix/update-openapi-docs
fix: update openapi specs
2025-08-11 13:56:33 -07:00
Alex Holliday c5dd16511d update openapi specs 2025-08-11 13:55:01 -07:00
Owaise 65f437f08e Merge remote-tracking branch 'origin' into Network-tab-implementation-FE 2025-08-09 12:21:21 +05:30
Owaise 47c231a214 Formating done. 2025-08-09 12:17:25 +05:30
Owaise f5d0b74ccf Fixed the processing to be done in backend now. 2025-08-09 12:14:25 +05:30
Alexander Holliday 17cac37967 Merge pull request #2736 from burak28/feat/add-game-server-support
Feat/add game server support
2025-08-08 15:11:06 -07:00
Burak Arslan b7e7028b6f Fix error handling and method assignment in requestGame function 2025-08-08 23:59:41 +03:00
Alexander Holliday ea3df7be42 Merge pull request #2749 from bluewave-labs/fix/gap-select-components
Fix/gap select components
2025-08-08 09:27:21 -07:00
karenvicent f0d7e82e22 Merge branch 'develop' into fix/gap-select-components 2025-08-07 18:53:58 -04:00
karenvicent c6d56b30e4 fix gaps in select components 2025-08-07 18:53:05 -04:00
Alexander Holliday 8225924da6 Merge pull request #2746 from vineet-channe/feature/yellow-range-infrastructure-monitors
feat: Add yellow warning range (50-80%) for infrastructure monitors
2025-08-07 14:54:00 -07:00
Alexander Holliday 6517e9ee6c Merge pull request #2748 from bluewave-labs/feat/fe/sidebar-theme
Feat: Theme Switch Removal from Sidebar
2025-08-07 14:53:15 -07:00
vineet-channe 10ad3397a3 docs(CustomGauge): remove threshold prop from documentation and usage example
- Removed all references to the unused  prop from the JSDoc and usage example in the CustomGauge component.
- Ensured PropTypes and documentation accurately
2025-08-08 03:03:23 +05:30
Br0wnHammer dd9468cb2e Remove unused Imports 2025-08-08 02:06:44 +05:30
Br0wnHammer 0b9ab0614f Tooltip Position 2025-08-08 01:31:20 +05:30
Br0wnHammer e13f495d71 Remove Redundant Stack 2025-08-08 01:11:27 +05:30
Br0wnHammer 029b8fac37 Feat: Remove Theme Toggle Button 2025-08-08 01:07:49 +05:30
Alexander Holliday 6e5ada79de Merge pull request #2742 from bluewave-labs/feat/register-page
Feat/register page
2025-08-07 09:20:07 -07:00
Alexander Holliday 59f050135a Merge pull request #2744 from bluewave-labs/fix/fe/appname-color
Fix: Checkmate Color
2025-08-07 09:15:09 -07:00
vineet-channe dc8b0aed9f chore: format CustomGauge with Prettier 2025-08-07 20:46:15 +05:30
vineet-channe 424df24bf3 feat: Add yellow warning range (50-80%) for infrastructure monitors
Fixes #2615
2025-08-07 20:35:24 +05:30
Br0wnHammer d451f725cd Redundant sx prop 2025-08-07 18:21:50 +05:30
Br0wnHammer d04b8fddb0 Fix: Appname Color 2025-08-07 18:04:58 +05:30
karenvicent bb592964ee Merge branch 'develop' into feat/register-page 2025-08-06 17:19:21 -04:00
karenvicent 786dd427bc feat: Update Register page to match new Login design for consistent UX 2025-08-06 17:17:44 -04:00
Burak Arslan 4c2c452b91 Improve validation and error handling on form field 2025-08-06 00:53:35 +03:00
Alexander Holliday 1765bf7e55 Merge pull request #2740 from vineet-channe/fix-diagnostic-page-design
fix: update diagnostic page design to match infrastructure page
2025-08-05 10:10:35 -07:00
Alexander Holliday e19de23770 Merge pull request #2693 from Owaiseimdad/Network-tab-implementation-infra
Network tab implementation infra, in api at backend
2025-08-05 09:54:17 -07:00
Owaise 765a1a24ed Added en.json based values and console removal. 2025-08-05 21:04:15 +05:30
Owaise e42b4b2808 fixed the minor changes for FE. 2025-08-05 20:46:42 +05:30
ThetaOI 65f6925864 Merge branch 'bluewave-labs:develop' into Network-tab-implementation-FE 2025-08-05 19:49:59 +05:30
ThetaOI 02b7dc080b Merge branch 'bluewave-labs:develop' into Network-tab-implementation-infra 2025-08-05 19:49:38 +05:30
Owaise d4e8381827 Renamed them as average values in the table. 2025-08-05 19:43:35 +05:30
vineet-channe de34710c90 fix: address review feedback
- Replace hardcoded button text with translation
- Fix status box translation to avoid showing removed title
- Add conditional rendering for optional gauge metrics
2025-08-05 17:35:40 +05:30
Alexander Holliday cd8752c1cb Merge pull request #2737 from Jesulayomy/fe-infra_monitor_upgrade
[Frontend]: Refactor Infrastructure monitor create Component
2025-08-04 10:05:06 -07:00
vineet-channe 86a0b0b60c fix: update diagnostic page design to match infrastructure page
- Remove 'System diagnostics' title and divider line
- Add infrastructure-style status boxes with dynamic status
- Update gauge components to use BaseContainer styling
- Match infrastructure page layout and spacing

Resolves #2725
2025-08-04 18:43:37 +05:30
Alexander Holliday fc5ccf1cf8 Merge pull request #2738 from bluewave-labs/feature/2564-add-custom-css-field
Add customCSS field to StatusPage model
2025-08-03 16:16:46 -07:00
mohadeseh safari 45352b62e1 feat(status-page): add customCSS field to StatusPage model 2025-08-03 18:51:12 -04:00
Burak Arslan 0ea3379397 Merge branch 'feat/add-game-server-support' of https://github.com/burak28/Checkmate into feat/add-game-server-support 2025-08-03 13:20:02 +03:00
Burak Arslan 281cbbc30f feat: add game server monitoring support
- Added a new `/games` route in `monitorRoute.js` to fetch all supported games.
- Implemented `getAllGames` method in `monitorController.js` using the `gamedig` library.
- Introduced `useFetchMonitorGames` hook in `monitorHooks.js` to fetch game data in the frontend.
- Updated `UptimeCreate` page to support game server monitoring:
  - Added a new monitor type `"game"`.
  - Dynamically fetches and displays game options using the new `/games` API.
- Removed hardcoded `GAMES` object and replaced it with dynamic fetching from the backend.
- Updated `NetworkService.js` with a new `getMonitorGames` method for API calls.

This update introduces game server monitoring functionality, allowing users to monitor game servers dynamically.
2025-08-03 13:19:55 +03:00
Alexander Holliday 0de9e067c6 Merge pull request #2728 from Mohansai-M/feat/incident-page-block-icons
Feat/incident page block icons
2025-08-02 19:58:49 -07:00
Alexander Holliday 77c00558aa Merge pull request #2735 from bluewave-labs/fix/sidebar
fix: sidebar
2025-08-02 19:50:17 -07:00
owiaseimdad 10c5463892 Fixed the tool tip. 2025-08-03 01:26:13 +05:30
owiaseimdad 7b963ca877 Network tab first implementation 2025-08-03 00:30:04 +05:30
Jesulayomy aa35e9211b [Infra]: Removed duplicate call to .find method on the errors object 2025-08-02 04:13:29 -04:00
Jesulayomy 2fc0eb9460 [Infra]: Added translations for some time blocks 2025-08-02 03:53:54 -04:00
Jesulayomy 91b4c976e5 [Infra]: Refactor Create Component, Added Pause and Remove tool bar 2025-08-02 03:53:11 -04:00
owiaseimdad 950c5fdc92 Formating done. 2025-08-02 12:42:35 +05:30
allanmaaz 114572024c Removed teh required true from network db. 2025-08-02 12:34:31 +05:30
Burak Arslan 8503b7ece4 Merge branch 'develop' into feat/add-game-server-support 2025-08-02 02:28:53 +03:00
Burak Arslan b53650a50f Run npm run format 2025-08-02 02:25:58 +03:00
Burak Arslan 9ad3b58fcd Added game server support 2025-08-02 02:04:06 +03:00
Alex Holliday 521c6988a5 add onclick to avatar 2025-08-01 15:58:01 -07:00
Alex Holliday 7900e1104f clean up nav item 2025-08-01 15:52:55 -07:00
Alex Holliday 035edd92f4 add proptypes, fix logo text animation 2025-08-01 15:45:43 -07:00
Alex Holliday 2c4c6a5024 extract role names 2025-08-01 15:43:12 -07:00
Alex Holliday 12747bd95b remove console 2025-08-01 15:36:33 -07:00
Alex Holliday 7c22663250 remove unused css and old sidebar 2025-08-01 15:31:03 -07:00
Alex Holliday eb4856901b extract function 2025-08-01 15:28:22 -07:00
Alex Holliday da905169e1 remove extra popup state 2025-08-01 15:22:44 -07:00
Alex Holliday fe9bf6375f fix sticky sidebar 2025-08-01 15:14:03 -07:00
Alex Holliday 800f988979 move components to component folder 2025-08-01 15:01:30 -07:00
Alex Holliday b4f42bb7d4 fix arrow icons 2025-08-01 14:58:30 -07:00
Alex Holliday e58c5fdb86 Merge branch 'develop' into fix/sidebar 2025-08-01 14:43:33 -07:00
Alex Holliday 4e3f552538 simplify layouts 2025-08-01 14:10:20 -07:00
Alex Holliday 2d01205f70 auth footer styling 2025-08-01 12:39:35 -07:00
Mohansai Mallineni ebfb25ebc0 Handled styling of new icons 2025-08-01 22:09:15 +05:30
Alexander Holliday d93fb48202 Merge pull request #2732 from bluewave-labs/improve/login-page
Improve/login page
2025-08-01 09:13:02 -07:00
Mohansai Mallineni a73d0ef3b4 format 2025-08-01 21:15:12 +05:30
Mohansai Mallineni 8274d4ffbe Handled styling of new icons 2025-08-01 21:11:46 +05:30
Mohansai Mallineni 48990b860f Handled styling of new icons 2025-08-01 21:09:51 +05:30
Alex Holliday b61a7ffc3c components 2025-07-31 15:37:40 -07:00
karenvicent 37a3028540 Merge branch 'develop' into improve/login-page 2025-07-31 16:33:12 -04:00
karenvicent 2e675f5dee feat: Enhance Login page design and fix UI details 2025-07-31 16:32:30 -04:00
Alexander Holliday 6a2a79c086 Merge pull request #2731 from bluewave-labs/hotfix/monitor-module
fix references
2025-07-31 13:25:12 -07:00
Alex Holliday a86e9ff480 fix references 2025-07-31 13:24:32 -07:00
Alex Holliday 3dc332010e start refactor 2025-07-31 12:47:36 -07:00
Alexander Holliday 61a1972c4c Merge pull request #2730 from bluewave-labs/fix/diagnostic-module
fix: diagnostic module
2025-07-31 12:06:30 -07:00
Alex Holliday 0779e5ea15 remove commented out imports 2025-07-31 12:02:52 -07:00
Alexander Holliday fab78f0cee Merge pull request #2729 from bluewave-labs/feat/settings-module
feat: settubgs module -> class
2025-07-31 12:01:45 -07:00
Alex Holliday ce49270cf8 remove unused diagnostic db module 2025-07-31 12:00:27 -07:00
Alex Holliday 7a190f7c69 module -> class 2025-07-31 11:57:55 -07:00
Mohansai Mallineni 126767ed5c Replaced existing icons with the provided svg's in the Incident page block area 2025-07-31 22:39:17 +05:30
Alexander Holliday 7d5302cfcf Merge pull request #2707 from singh-kanwarpreet/feature/global-thresholds
Feature/global thresholds
2025-07-31 09:31:14 -07:00
Alexander Holliday 87d3d6a8f0 Merge pull request #2724 from bluewave-labs/feat/recovery-module
feat: recovery module -> class
2025-07-31 09:18:56 -07:00
Alex Holliday 927e8a88b8 remove console log 2025-07-31 09:18:12 -07:00
singh-kanwarpreet 73a6277feb Refactored Code 2025-07-31 16:04:41 +05:30
Mohansai Mallineni 94a7044b7b Replaced existing icons with the provided svg's in the Incident page block area 2025-07-31 05:07:42 +05:30
Alex Holliday 54d3392cf1 remove commented out imports 2025-07-30 15:35:16 -07:00
Alexander Holliday f1c9aad4e6 Merge pull request #2723 from bluewave-labs/feat/pagespeed-module
feat: pagespeed module->class
2025-07-30 15:34:32 -07:00
Alex Holliday 48cf8a5e73 module -> class 2025-07-30 15:34:03 -07:00
Alex Holliday b5f4c04165 module->class 2025-07-30 15:21:22 -07:00
Alexander Holliday a9c2549376 Merge pull request #2722 from bluewave-labs/feat/notification-module
feat: notification module -> class
2025-07-30 15:17:19 -07:00
Alex Holliday 7bf96f6194 module -> class 2025-07-30 15:14:36 -07:00
Alexander Holliday ae40cb8a54 Merge pull request #2721 from bluewave-labs/feat/network-check-module
feat: NetworkCheck module -> class
2025-07-30 15:06:50 -07:00
Alex Holliday 16ebe4b1ab module -> class 2025-07-30 15:06:19 -07:00
Alexander Holliday c7914a1c4f Merge pull request #2720 from bluewave-labs/feat/monitor-module
feat: monitor module
2025-07-30 15:06:03 -07:00
Alex Holliday 6b12246ec0 Merge branch 'develop' into feat/monitor-module 2025-07-30 15:00:33 -07:00
Alexander Holliday 4a42b6eb43 Merge pull request #2719 from bluewave-labs/feat/maintenance-window-module
feat: module -> class
2025-07-30 14:59:46 -07:00
Alex Holliday de742068a7 module -> class 2025-07-30 12:25:12 -07:00
Alex Holliday b383ce5b11 Merge branch 'develop' into feat/maintenance-window-module 2025-07-30 11:39:23 -07:00
Alexander Holliday ccb424eafe Merge pull request #2718 from bluewave-labs/feat/hardware-module
module -> class
2025-07-30 11:38:34 -07:00
Alex Holliday 585fb4f700 module -> class 2025-07-30 11:36:19 -07:00
Alex Holliday 00f5eaf44d module -> class 2025-07-30 11:27:29 -07:00
singh-kanwarpreet 8bacbcde89 Removed Unnecassary command 2025-07-30 23:35:51 +05:30
Alexander Holliday 245e436a88 Merge pull request #2717 from bluewave-labs/feat/user-module-class
feat: refactor user module into class
2025-07-30 10:54:41 -07:00
singh-kanwarpreet 2d6fdcb1e2 Made code readable 2025-07-30 23:14:35 +05:30
Alex Holliday 8adad64248 refactor module into class 2025-07-30 10:41:53 -07:00
Alexander Holliday e7cf6d385c Merge pull request #2714 from bluewave-labs/feat/statuspage-module
feat: refactor status page module into class
2025-07-30 10:22:46 -07:00
Alex Holliday 753a904c94 refacotr status page module into class 2025-07-30 10:20:33 -07:00
Alexander Holliday 0ccbd17709 Merge pull request #2713 from bluewave-labs/hotfix/crypto
fix crypto dependency
2025-07-30 09:19:07 -07:00
Alex Holliday 78a5b2e666 fix crypto dependency 2025-07-30 09:18:16 -07:00
singh-kanwarpreet 5e46a9f2d5 Pushed global thresholds inside create if branch 2025-07-30 19:38:23 +05:30
singh-kanwarpreet 0ecbf48f5d client/src/Pages/Infrastructure/Create/index.jsx 2025-07-30 19:18:24 +05:30
singh-kanwarpreet d4ecb26ffe Updated UseEffect 2025-07-30 18:30:04 +05:30
singh-kanwarpreet e04ac77ddd Ran npm start format 2025-07-30 16:39:41 +05:30
Alexander Holliday cd3702f5b5 Merge pull request #2712 from bluewave-labs/feat/invite-module
feat: refactor invite module into class
2025-07-29 20:01:39 -07:00
Alex Holliday ca02ed9d18 refactor invite module into class 2025-07-29 19:53:11 -07:00
Alexander Holliday f934bbf829 Merge pull request #2711 from bluewave-labs/fix/check-module
fix: convert checkModule to class for dependency injection
2025-07-29 12:04:44 -07:00
singh-kanwarpreet da7d9c02c8 Used Parent State And Set Minor Changes 2025-07-30 00:25:53 +05:30
Alex Holliday e55d8618bd convert checkModule to class for dependency injection 2025-07-29 11:51:06 -07:00
singh-kanwarpreet af010e138d Some changes in ui 2025-07-30 00:01:27 +05:30
Alexander Holliday 9d94b71583 Merge pull request #2664 from Jesulayomy/fe-uptime_monitor_config
[Frontend]: Refactor Uptime monitor Create & Configure components
2025-07-29 11:15:55 -07:00
Alexander Holliday 38d31192ed Merge pull request #2710 from bluewave-labs/fix/sort-json
sort en.json
2025-07-29 11:14:30 -07:00
Alex Holliday a0799874e4 sort en.json 2025-07-29 11:14:06 -07:00
Alexander Holliday 5ec2500310 Merge pull request #2698 from bluewave-labs/improve/light-mode-ui
Improve/light mode UI
2025-07-29 11:11:17 -07:00
karenvicent 0bd9aa1e3f Merge branch 'develop' into improve/light-mode-ui 2025-07-29 13:45:14 -04:00
karenvicent 20dd3ead3e change background and boder colors to tertiry pallette 2025-07-29 13:43:47 -04:00
Alexander Holliday b30a512d6e Merge pull request #2709 from bluewave-labs/hotfix/secrets
hotfix: replace missing secrets
2025-07-29 10:09:17 -07:00
Alex Holliday e63faebcb8 replace missing secrets 2025-07-29 10:08:45 -07:00
Alexander Holliday 7e5cb1facc Merge pull request #2708 from bluewave-labs/hotfix/logger
add legacy logger
2025-07-29 10:01:49 -07:00
Alex Holliday 10c822c5d9 add legacy logger 2025-07-29 10:01:20 -07:00
Alexander Holliday 8761b3e28d Merge pull request #2704 from bluewave-labs/fix/settings
fix/settings
2025-07-29 09:39:08 -07:00
Alexander Holliday 19cccbbcad Merge pull request #2705 from satnam72/develop
Fix the broken breadcrumbs link
2025-07-29 09:38:25 -07:00
singh-kanwarpreet dbea93f9ba Improved Code Quality 2025-07-29 21:35:30 +05:30
karenvicent 4629014922 Merge branch 'develop' into improve/light-mode-ui 2025-07-29 10:08:29 -04:00
karenvicent 24e7b1c224 add darmode animation to fallback component 2025-07-29 10:07:37 -04:00
singh-kanwarpreet ad06544a64 Removed Consecutive same statements 2025-07-29 16:05:53 +05:30
singh-kanwarpreet 8d105e636d Updated Global THreshold Code Quality 2025-07-29 16:05:53 +05:30
singh-kanwarpreet bf5d7f3c53 Adding Global Threshold Settings 2025-07-29 16:05:53 +05:30
satnam72 c5cd3512af Fixed Formatting issue
Ran the npm run format to fix formatting
2025-07-29 02:56:41 -04:00
satnam72 2802cce6f1 Fix the breadcrumbs route
- Updated breadcrumbs to use `/status/uptime/${url}` instead of `/status/${url}`
2025-07-28 23:40:24 -04:00
Alex Holliday 6e49e19ba1 refactor serviceRegistry 2025-07-28 15:25:33 -07:00
Alexander Holliday 0c6d635b67 Merge pull request #2700 from bluewave-labs/translation-update-75
🌐 Updated Translations from POEditor
2025-07-28 15:06:17 -07:00
Alex Holliday de80a530e5 refactor logger 2025-07-28 15:03:28 -07:00
GitHub Action 63b106d844 feat: translations updated from POEditor 2025-07-28 21:42:35 +00:00
Alexander Holliday ab96e321c9 Merge pull request #2699 from bluewave-labs/feat/ja
Feat/ja
2025-07-28 14:41:52 -07:00
Alex Holliday 0b7d1abb71 add ja 2025-07-28 14:41:24 -07:00
Alex Holliday 592c184ffe add ja 2025-07-28 14:40:10 -07:00
Alex Holliday f3e50120ee appSettings -> envSettings 2025-07-28 14:37:18 -07:00
Gorkem Cetin (BWL) 850510b7e8 Update README.md 2025-07-28 17:30:33 -04:00
karenvicent d1e9a1221f Merge remote-tracking branch 'origin/develop' into improve/light-mode-ui 2025-07-28 17:08:31 -04:00
karenvicent 56ea28aa83 run format 2025-07-28 17:05:22 -04:00
Alexander Holliday 140ae1d049 Merge pull request #2694 from bluewave-labs/feat/rate-limiting
feat: add rate limiting
2025-07-28 13:48:59 -07:00
Alex Holliday 6025323c4b less aggressive auth rate limiting 2025-07-28 13:48:09 -07:00
karenvicent 261c83158b Merge remote-tracking branch 'origin/develop' into improve/light-mode-ui 2025-07-28 16:33:20 -04:00
karenvicent 1a5f16c4e3 improves in light mode 2025-07-28 16:28:44 -04:00
Owaise Imdad ab0bf5ca5f Fixed the names and err out and err in . 2025-07-29 00:42:55 +05:30
Owaise Imdad 158b66e43b Merge branch 'develop' of https://github.com/Owaiseimdad/Checkmate into Network-tab-implementation-infra 2025-07-29 00:30:46 +05:30
Alex Holliday ec99b5e9b4 add rate limiting 2025-07-28 12:00:09 -07:00
Owaise Imdad 19fe41d738 reverting the docker dev changes I made. 2025-07-29 00:22:14 +05:30
Owaise Imdad 5c147562b3 Added net to be returned from api in backend for infra. 2025-07-29 00:17:34 +05:30
Alexander Holliday 051c17f454 Merge pull request #2692 from bluewave-labs/feat/status-page-paintenance
feat: add isMaintenance field to monitors for status page queries
2025-07-28 11:40:30 -07:00
Alex Holliday ab8868e6c4 add an field to the monitor for status page queries 2025-07-28 11:35:49 -07:00
Jesulayomy b5b600db1f [Merge]: Merged commits from develop into branch fe-uptime_monitor_config 2025-07-28 13:09:09 -04:00
Alexander Holliday 41f917ddf6 Merge pull request #2688 from bluewave-labs/feat/server-refactor
feat: server refactor
2025-07-28 09:01:39 -07:00
Alex Holliday e5e02b553a fix dependencies 2025-07-27 19:51:39 -07:00
Owaise Imdad 729f107af2 Merge branch 'develop' of https://github.com/Owaiseimdad/Checkmate into develop 2025-07-27 03:47:49 +05:30
Owaise Imdad 8bfb187ef0 FIX merge conflicts 2025-07-27 03:47:30 +05:30
Alex Holliday 1c47c8ce2c move to src 2025-07-25 15:33:48 -07:00
Alex Holliday acc910cbc0 refactor server 2025-07-25 15:15:14 -07:00
Jesulayomy 1aedf21566 [Uptime]: Added PropType validation, passed isClone directly as prop 2025-07-25 13:37:17 -04:00
Alexander Holliday 6cb538ce69 Merge pull request #2681 from bluewave-labs/feat/error-service
feat: error service
2025-07-25 10:36:10 -07:00
Alex Holliday cafd9f9899 remove empty file 2025-07-25 10:29:40 -07:00
Alex Holliday bb2ab8e08a format 2025-07-25 10:29:04 -07:00
Alex Holliday 3059e730e2 type errors 2025-07-25 10:19:22 -07:00
Alex Holliday dc321580c3 refactor out common dependenceis 2025-07-25 10:16:29 -07:00
Jesulayomy 6cfa77a335 [Uptime]: Add clone Route and restore cloning logic 2025-07-25 11:11:39 -04:00
Alex Holliday ddf3e8251d add an error service, base controller 2025-07-24 15:06:46 -07:00
Alexander Holliday 0cfe9d60fb Merge pull request #2678 from bluewave-labs/feat/monitor-service
feat: monitor service
2025-07-24 13:57:25 -07:00
Alex Holliday 19d3ae119c added safe var access 2025-07-24 13:51:00 -07:00
Alex Holliday d59e2796e7 refactor to monitor service 2025-07-24 13:50:21 -07:00
Gorkem Cetin (BWL) 12ff66fd3e Merge pull request #2667 from ferCancholaCruz/add-readme-es
Spanish version of Readme File. Agrega traducción al español del README
2025-07-24 14:16:25 -04:00
Fernando Canchola Cruz 35270e2761 Update README.es.md
Fixed readme.es.md
2025-07-24 11:38:45 -06:00
Jesulayomy 47021fa55c [Uptime]: removed formatAndSet, added displayInterval, moved UI logic out of component view 2025-07-24 13:38:14 -04:00
Jesulayomy 89f5be2cc9 [Uptime]: Moved hook, removed duplicate imports, teporarily disabled formatAndSet function due to rerenders 2025-07-23 21:13:19 -04:00
Jesulayomy d3493a0af6 [Uptime]: Used latest formatting rules 2025-07-23 20:01:39 -04:00
Jesulayomy 157343d152 [Locales]: Sorted translations, fixed a duplicate key from the original branch 2025-07-23 19:59:45 -04:00
Jesulayomy 5f36543d44 [Uptime]: Removed UptimeConfigure component, added extra translations for Uptime and other monitor pages 2025-07-23 19:22:09 -04:00
Jesulayomy a004b7b450 [Uptime]: Merged Configure into Create Component 2025-07-23 19:20:37 -04:00
Jesulayomy 56d8224fb9 [Uptime]: Added skeleton file 2025-07-23 19:15:28 -04:00
Alexander Holliday dd6e7b7e7e Merge pull request #2677 from bluewave-labs/hotfix/clone-monitor
fix: reimplement clone monitor, resolves #2676
2025-07-23 15:32:23 -07:00
Alex Holliday 6d5034e2a5 reimplement clone monitor 2025-07-23 15:31:26 -07:00
Alexander Holliday ed6882ba95 Merge pull request #2675 from bluewave-labs/feat/mw-service
feat: add maintenance window service
2025-07-23 15:15:25 -07:00
Alex Holliday b3f8d77238 add maintenance window service 2025-07-23 15:14:47 -07:00
Alexander Holliday c287da92d9 Merge pull request #2674 from bluewave-labs/feat/invite-service
feat: add invite service
2025-07-23 15:00:51 -07:00
Alex Holliday aea79fec2e add invite service 2025-07-23 14:58:44 -07:00
Alexander Holliday 3239f555a1 Merge pull request #2673 from bluewave-labs/feat/diagnostic-service
feat: diagnostic service
2025-07-23 14:31:51 -07:00
Alex Holliday da49e15589 jsdocs, move observer 2025-07-23 14:31:40 -07:00
Alex Holliday cfebc770fd add diagnostic service 2025-07-23 14:28:16 -07:00
Alexander Holliday ffef14957f Merge pull request #2671 from bluewave-labs/feat/check-service
feat: check service
2025-07-23 14:22:25 -07:00
Alexander Holliday 05666f70e0 Merge pull request #2672 from bluewave-labs/fix/validation
Fix/validation
2025-07-23 14:21:02 -07:00
Alex Holliday c1dc575f53 reset server validation 2025-07-23 14:17:47 -07:00
Alex Holliday bf861700fa resest client validation 2025-07-23 14:15:43 -07:00
Alexander Holliday 627c2174a9 Merge pull request #2670 from bluewave-labs/feat/fe/resolve-monitor-incidents
Feat: Selected Monitor Incidents Resolve
2025-07-23 14:03:34 -07:00
Br0wnHammer b27eccec88 Merge branch 'develop' into feat/fe/resolve-monitor-incidents 2025-07-24 02:15:00 +05:30
Alex Holliday 8c0dfb2cc5 add a check service 2025-07-23 13:41:10 -07:00
Br0wnHammer c766dc9f00 JSDOC for both network calls 2025-07-24 02:06:55 +05:30
Br0wnHammer 7236e85321 Removal of old code 2025-07-24 01:56:05 +05:30
Br0wnHammer b7093b5de5 Feat: One Hook for Incidents Ack 2025-07-24 01:54:58 +05:30
Br0wnHammer 3650c400ca Feat: Monitor Incidents Resolve 2025-07-24 01:47:42 +05:30
Br0wnHammer 4933d11dfd Network Service: Monitor Check Func 2025-07-24 01:42:46 +05:30
Alexander Holliday 08128279b4 Merge pull request #2669 from bluewave-labs/feat/service-refactor
feat: service refactor
2025-07-23 13:11:52 -07:00
Alex Holliday 181e149028 Merge branch 'develop' into feat/service-refactor 2025-07-23 13:09:48 -07:00
Alexander Holliday 9bb9265a19 Merge pull request #2668 from bluewave-labs/feat/user-service
feat: user service
2025-07-23 13:08:33 -07:00
Alex Holliday 65f232161f sync package lock 2025-07-23 13:06:23 -07:00
Br0wnHammer df97b93a9a Button Text Dynamic 2025-07-24 01:35:31 +05:30
Alex Holliday f58d05f1b3 format 2025-07-23 13:04:26 -07:00
Alex Holliday 06fa79e09d refactor service dir structure 2025-07-23 13:03:57 -07:00
Gorkem Cetin (BWL) 6da52adc74 Trying to fix anchors 2025-07-23 15:41:44 -04:00
Alex Holliday 0b4ec46f79 add updated JSdoc 2025-07-23 12:12:22 -07:00
Alex Holliday 6e321afa72 add remaining methods to service 2025-07-23 12:09:23 -07:00
Alex Holliday a08ded4093 move checksuperadmin to userservice 2025-07-23 11:48:47 -07:00
Alex Holliday e76b44fcba fix validaiton for editing user 2025-07-23 11:47:21 -07:00
Alex Holliday 1fa2dbcf85 Add user serivce, move register, login, and edit 2025-07-23 11:47:13 -07:00
Alex Holliday 74fd20cfa8 move checksuperadmin 2025-07-23 11:46:59 -07:00
Fernando Canchola Cruz f90b5bab4b Agrega traducción al español del README 2025-07-23 12:36:07 -06:00
Alexander Holliday 812cff0fd9 Merge pull request #2559 from bluewave-labs/feat/super-simple-queue
feat: super simple queue
2025-07-23 10:38:13 -07:00
Alex Holliday 7127c28c35 bump dependency 2025-07-23 10:35:32 -07:00
Alex Holliday 4f186a6b49 Merge branch 'develop' into feat/super-simple-queue 2025-07-23 10:35:11 -07:00
Alexander Holliday fe76416059 Merge pull request #2666 from bluewave-labs/hotfix/import
fix import path
2025-07-23 10:13:12 -07:00
Alex Holliday ac2b7bd65d fix import path 2025-07-23 10:12:47 -07:00
Alexander Holliday 604b20855b Merge pull request #2663 from bluewave-labs/feat/fe/infrastructure-disk-ui-overflow
Fix: Infrastructure disks overflow
2025-07-23 09:45:56 -07:00
Alexander Holliday e157ee7c53 Merge pull request #2661 from bluewave-labs/feat/edit-users
feat: edit users, resolves #2659
2025-07-23 09:43:27 -07:00
Alex Holliday 91ccfe26b7 Merge branch 'feat/edit-users' of github.com:bluewave-labs/checkmate into feat/edit-users 2025-07-23 09:37:52 -07:00
Alex Holliday de88e1d442 return early if delete superadmin 2025-07-23 09:37:37 -07:00
Alexander Holliday baf5967dca Merge branch 'develop' into feat/edit-users 2025-07-23 09:37:04 -07:00
Alexander Holliday df6a1b39e0 Merge pull request #2662 from bluewave-labs/feat/fallback-refactor
Reopen Feat/fallback refactor
2025-07-23 09:33:47 -07:00
Br0wnHammer 8fcaf721ad Fix: Infrastructure disks overflow 2025-07-23 15:33:09 +05:30
karenvicent 22d17708cd format 2025-07-22 16:46:43 -04:00
Alex Holliday 457408006b sort en.json 2025-07-22 13:33:11 -07:00
Alex Holliday f74289fe49 format 2025-07-22 13:32:26 -07:00
Alex Holliday 86ece89cec Merge branch 'develop' into feat/edit-users 2025-07-22 13:32:03 -07:00
Alex Holliday 22826afd21 apply validation 2025-07-22 13:26:45 -07:00
Alex Holliday 73f2f29c2a add valid roles 2025-07-22 13:26:01 -07:00
karenvicent a6eb2735aa Merge branch 'develop' into feat/fallback-refactor 2025-07-22 16:23:37 -04:00
Alex Holliday c4d42dd9e3 add db 2025-07-22 12:27:41 -07:00
Alex Holliday e047309812 add controller methods 2025-07-22 12:27:33 -07:00
Alex Holliday c11da04e13 add edit user routes 2025-07-22 12:27:17 -07:00
Alex Holliday 4f68a76674 implement frontend of user edit 2025-07-22 12:27:06 -07:00
karenvicent b2fdb11195 run format 2025-07-22 14:54:41 -04:00
Mohansai Mallineni 97082b30b9 feat(i18n): implement new language mappings and initialize translation files 2025-07-22 13:59:19 -04:00
Alex Holliday 43ac0174d1 format 2025-07-22 09:23:26 -07:00
Alex Holliday cb02bb0de7 Merge branch 'develop' into feat/fallback-refactor 2025-07-22 09:23:09 -07:00
Alexander Holliday 9c9eaca990 Merge pull request #2646 from Mohansai-M/feat/i18n-new-lang-support
feat(i18n): implement new language mappings and initialize translatio…
2025-07-22 09:21:36 -07:00
Alexander Holliday 8336ea199d Merge pull request #2658 from bluewave-labs/fix/protect-demo-user
prevent demo user from being deleted
2025-07-21 15:05:57 -07:00
Alex Holliday 48848d1d43 prevent demo user from being deleted 2025-07-21 15:05:17 -07:00
Alexander Holliday 1eea0b6862 Merge pull request #2632 from Yasir-Rafique/validationRemoval_monitorURL
Relax monitor URL validation
2025-07-21 14:52:12 -07:00
Alexander Holliday 2977c1f6db Merge pull request #2641 from miguemely/develop
Increase helm chart support for different cluster types + CORS Fixes
2025-07-21 14:45:40 -07:00
Alexander Holliday c70ee26c8a Merge pull request #2657 from bluewave-labs/fix/notification-routes
fix auth for notification controller
2025-07-21 14:11:52 -07:00
Alex Holliday 1b085236c7 fix aith for notification controller 2025-07-21 14:11:09 -07:00
Alexander Holliday 78b371bfad Merge pull request #2656 from bluewave-labs/fix/monitor-routes
fix: monitor routes
2025-07-21 13:58:33 -07:00
Alex Holliday fdff3be316 fix team auth 2025-07-21 13:58:01 -07:00
Alex Holliday f2037bf5fe fix route 2025-07-21 13:46:06 -07:00
karenvicent a28a8fe5f9 Merge branch 'develop' into feat/fallback-refactor 2025-07-21 16:19:49 -04:00
karenvicent 62f94cfb20 refactor: extract Fallback component into smaller reusable components 2025-07-21 16:13:18 -04:00
Alexander Holliday 3e75972b02 Merge pull request #2654 from bluewave-labs/fix/mw-routes
fix: mw routes
2025-07-21 12:28:44 -07:00
Alex Holliday d6d3b53846 add team auth to mw routes 2025-07-21 12:27:53 -07:00
Alex Holliday 87f843df1c add getmonitorsbyids 2025-07-21 12:27:43 -07:00
Alex Holliday 5dbec377e5 reorder routes 2025-07-21 12:26:38 -07:00
Alexander Holliday a489c9ff45 Merge pull request #2653 from ajhollid/fix/diagnositc-routes
fix: remove unused routes, add authentication
2025-07-21 12:04:52 -07:00
Alex Holliday 4d989746fd remove unused routes, add authentication 2025-07-21 12:04:16 -07:00
Alexander Holliday ba48461417 Merge pull request #2652 from bluewave-labs/fix/check-routes
fix auth for check routes
2025-07-21 11:54:44 -07:00
Alex Holliday 55e2d5c0e3 fix auth for check routes 2025-07-21 11:54:04 -07:00
Alexander Holliday 8864f9799f Merge pull request #2651 from bluewave-labs/fix/auth-routes
fix: auth routes
2025-07-21 11:06:42 -07:00
Alex Holliday e2ddffe24b remove userId param from routes 2025-07-21 11:05:48 -07:00
Alex Holliday 12b7819791 remove user ID param from rotues 2025-07-21 11:05:10 -07:00
Alex Holliday bfa6832beb format 2025-07-21 10:34:16 -07:00
Alex Holliday 846564a17e reorder routes 2025-07-21 10:34:04 -07:00
Alexander Holliday 46d7009837 Merge pull request #2650 from bluewave-labs/fix/route-reorder
fix: reorder routes for correctness
2025-07-21 09:56:50 -07:00
Alex Holliday 0c04db3299 reorder routes for correctness 2025-07-21 09:55:49 -07:00
Miguel Rodriguez 2d437e2016 coderabbit fix: Add HTTPS to CORS origins 2025-07-20 13:30:01 -04:00
Miguel Rodriguez 37655a64c0 fix: Set CORS headers with a split setup on helm. 2025-07-20 13:22:01 -04:00
Miguel Rodriguez 4921b17f11 fix: docker.sock doesn't exist on many K8S Servers 2025-07-20 12:57:16 -04:00
Gorkem Cetin (BWL) dae0d6cc42 Update README.md 2025-07-20 11:50:34 -04:00
Mohansai Mallineni c392933e55 feat(i18n): implement new language mappings and initialize translation files 2025-07-20 20:08:06 +05:30
Gorkem Cetin (BWL) c915137ed7 Add reddit group 2025-07-20 10:11:52 -04:00
Yasir Rafique 945cff978b Use URL.canParse for URL validation; add strict regex fallback (maintainer feedback) 2025-07-19 06:53:01 +01:00
Alexander Holliday ae21b728bc Merge pull request #2625 from bluewave-labs/feat/toast-refactor
feat: toast refactor
2025-07-18 11:22:32 -07:00
Yasir Rafique 8ffd9a5e2a Tighten Docker hostname regex for monitor URL validation 2025-07-18 11:58:27 +01:00
Yasir Rafique b823f0f7db Relax monitor URL validation for Docker and local hostnames (fixes #2605) 2025-07-18 11:40:20 +01:00
Yasir Rafique 066df2c64f Relax monitor URL validation 2025-07-18 10:59:02 +01:00
Alexander Holliday 200a933647 Merge pull request #2621 from bluewave-labs/fix/consistent-error-handling
fix: imporved and consistent error handling
2025-07-17 09:37:51 -07:00
Alex Holliday 4cd5cd4bae fix close icon placement for toasts without title 2025-07-16 12:51:52 -07:00
Alexander Holliday bfba1d148e Merge pull request #2592 from bluewave-labs/feature/all-maintenance-on-window
Add "All Monitors" as an option for Maintenance windows  #2579
2025-07-16 10:38:47 -07:00
Alex Holliday c58801f6f7 update toastUtils to use Toast component 2025-07-16 10:33:06 -07:00
Alex Holliday 616927d388 Add new toast component 2025-07-16 10:32:19 -07:00
shanika Jayawardane 1cbebf322b remove unwanted code 2025-07-16 11:06:05 -06:00
Alexander Holliday a46c032979 Merge pull request #2617 from career-yashaswee/bug/2603-url-validation-email-format
(fix): implemented email and webhook url validation with proper error and toast creation
2025-07-16 09:16:12 -07:00
Alexander Holliday f03f94c3dd Merge pull request #2616 from bluewave-labs/fix/forms-margins-infrastructure-alignment
Fix/forms margins infrastructure alignment
2025-07-16 08:57:06 -07:00
karenvicent 1cee10842e apply style to MuiSelect in globalTheme 2025-07-16 10:36:52 -04:00
Br0wnHammer 55e9c74031 Fix: Page State Reset in Infra 2025-07-16 10:29:22 -04:00
Alexander Holliday 45a9495a51 Merge pull request #2624 from bluewave-labs/fix/fe/search-page-reset
Fix: Page State Reset in Infra
2025-07-16 06:33:50 -07:00
Br0wnHammer c785704dd8 Fix: Page State Reset in Infra 2025-07-16 12:29:18 +05:30
Alex Holliday 38bb915144 add error handling util, update middleware and controllers 2025-07-15 16:33:35 -07:00
Alexander Holliday e2f99f52db Merge branch 'develop' into fix/forms-margins-infrastructure-alignment 2025-07-15 15:22:43 -07:00
Alexander Holliday c54feb9306 Merge pull request #2620 from bluewave-labs/feat/diagnostics
Feat/diagnostics
2025-07-15 15:21:15 -07:00
Alexander Holliday e11b09feac Merge pull request #2600 from career-yashaswee/feat/2598-search-timezone-ux
Feat/2598 Search Timezone UX Addition
2025-07-15 13:05:34 -07:00
Yashaswee Kesharwani fe44bc989a fix: use-memo dependency array and prop-types error 2025-07-16 00:59:22 +05:30
karenvicent 93bf98fe72 Merge branch 'develop' into fix/forms-margins-infrastructure-alignment 2025-07-15 14:42:20 -04:00
karenvicent 5a2626b38b feat: apply styleOverrides for MuiSelect in base theme 2025-07-15 14:39:41 -04:00
Alexander Holliday 5d57138ea8 Merge pull request #2619 from mertssmnoglu/helm-syntax-error
fix: correct syntax error in secrets.yaml template #2618
2025-07-15 10:11:31 -07:00
Mert Şişmanoğlu 20519afe17 fix: correct syntax error in secrets.yaml template #2618
Signed-off-by: Mert Şişmanoğlu <mertssmnoglu@gmail.com>
2025-07-15 20:01:38 +03:00
Alexander Holliday f23f7df147 Merge pull request #2595 from bluewave-labs/feat/diagnostics
feat: diagnostics tab
2025-07-15 09:46:31 -07:00
Yashaswee Kesharwani b89ab5a192 (fix): implemented email and webhook url validation with proper error and toast creation #2603 2025-07-15 22:13:31 +05:30
karenvicent 35c59dc53b Merge branch 'develop' into fix/forms-margins-infrastructure-alignment 2025-07-15 09:03:56 -04:00
shanika Jayawardane 870cd6407e remove the drag and drop fuction for list and remove dublicate code 2025-07-14 23:44:50 -06:00
Alex Holliday f62350fea0 add refresh button 2025-07-14 16:26:34 -07:00
Alex Holliday 39779b9dd5 refacotr interval to be self documenting 2025-07-14 16:26:24 -07:00
Alexander Holliday 9716e64c93 Merge pull request #2612 from bluewave-labs/develop
Develop -> Master
2025-07-14 14:23:40 -07:00
karenvicent c86c5d8235 Merge branch 'develop' into fix/forms-margins-infrastructure-alignment 2025-07-13 20:25:23 -04:00
karenvicent 86160a103d feat(forms): standardized label-to-input spacing in forms via new FieldWrapper component 2025-07-13 20:23:10 -04:00
Yashaswee Kesharwani 2bb7818116 fix: replaced remaining Selects with Search in Settings Tab - optimised with useMemo and standardised the no unit found 2025-07-13 03:52:16 +05:30
shanika Jayawardane 39d666df6b add the checkboxes for dropdown list and remove searchbox list inside it 2025-07-12 15:13:24 -06:00
Yashaswee Kesharwani a7e26d553f chore: format code using npm run format 2025-07-12 23:22:42 +05:30
Yashaswee Kesharwani 95e82cf254 feat: resolve #2598 improve timezone search UX 2025-07-12 23:17:41 +05:30
Alex Holliday 07c8334248 Merge branch 'develop' into feat/super-simple-queue 2025-07-11 13:51:14 -07:00
Alex Holliday 5b395b96cd implemet stats 2025-07-11 11:23:50 -07:00
Alex Holliday b1b4fd3f64 add 1s cpu usage metric, return standard units of ms and bytes 2025-07-11 11:23:36 -07:00
shanika Jayawardane 6e76e29768 add the translation to selectAll 2025-07-11 07:59:12 -06:00
shanika Jayawardane 2c1a01bb6a add the All select option to monitor maintenance window for all monitors same as status pages 2025-07-11 07:44:02 -06:00
Alex Holliday 1cbec8220d add diagnostic page 2025-07-10 16:08:04 -07:00
Alex Holliday 1456c4f749 bump version 2025-07-10 16:06:38 -07:00
Alex Holliday 881944ab98 Merge branch 'develop' into feat/super-simple-queue 2025-07-10 14:45:47 -07:00
Alex Holliday cb2016a5dc update queue version 2025-07-08 11:04:20 -07:00
Alex Holliday 9040a99853 Merge branch 'develop' into feat/super-simple-queue 2025-07-08 11:02:14 -07:00
Alexander Holliday eb8cf1b6da Merge pull request #2584 from bluewave-labs/develop
bump version
2025-07-08 09:51:08 -07:00
Alexander Holliday c633a9ad8d Merge pull request #2582 from bluewave-labs/develop
develop -> master
2025-07-07 15:38:46 -07:00
Alex Holliday c3f5d249a0 use latest library 2025-07-07 08:55:57 -07:00
Alex Holliday ce103c6b59 format 2025-06-27 13:51:39 +08:00
Alex Holliday 38f5c41272 implement flush 2025-06-27 13:45:18 +08:00
Alex Holliday ca13185d84 convert monitor to object 2025-06-27 13:40:41 +08:00
Alex Holliday f1c6beacf2 use super-simple-queue 2025-06-27 12:38:06 +08:00
Alexander Holliday 923eda1307 Merge pull request #2545 from bluewave-labs/develop
develop -> master
2025-06-25 12:42:33 +08: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
400 changed files with 30902 additions and 18160 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
release_notes: false
+3 -1
View File
@@ -6,7 +6,9 @@ import { URLSearchParams } from "url";
// POEditor API information
const API_TOKEN = process.env.POEDITOR_API;
const PROJECT_ID = process.env.POEDITOR_PROJECT_ID;
const LANGUAGES = (process.env.LANGUAGES || "tr,en").split(",");
const LANGUAGES = (
process.env.LANGUAGES || "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn,th"
).split(",");
const EXPORT_FORMAT = process.env.EXPORT_FORMAT || "key_value_json";
// POEditor API endpoint
+2 -23
View File
@@ -16,7 +16,7 @@ jobs:
- name: Install client dependencies
working-directory: client
run: npm ci
run: npm install
- name: Check client formatting
working-directory: client
@@ -34,29 +34,8 @@ jobs:
- name: Install server dependencies
working-directory: server
run: npm ci
run: npm install
- 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."
@@ -79,18 +79,6 @@ jobs:
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:
-12
View File
@@ -74,18 +74,6 @@ jobs:
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:
+1 -1
View File
@@ -7,7 +7,7 @@ on:
languages:
description: "Languages to synchronize (comma separated, e.g.: tr,en,es)"
required: false
default: "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr"
default: "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn,th"
format:
description: "Export format (key_value_json or json)"
required: false
-11
View File
@@ -71,17 +71,6 @@ jobs:
- name: Push MongoDB Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:mongo-demo
- name: Build Redis Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:redis-demo \
-f ./docker/prod/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-demo
deploy-to-demo:
needs: docker-build-and-push-server
runs-on: ubuntu-latest
-11
View File
@@ -71,17 +71,6 @@ jobs:
- name: Push MongoDB Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:mongo-staging
- name: Build Redis Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:redis-staging \
-f ./docker/staging/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-staging
deploy-to-staging:
needs: docker-build-and-push-server
runs-on: ubuntu-latest
+1
View File
@@ -0,0 +1 @@
.vscode
+172
View File
@@ -0,0 +1,172 @@
<p align=center> <a href="https://trendshift.io/repositories/12443" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12443" alt="bluewave-labs%2Fcheckmate | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a></p>
![](https://img.shields.io/github/license/bluewave-labs/checkmate)
![](https://img.shields.io/github/repo-size/bluewave-labs/checkmate)
![](https://img.shields.io/github/commit-activity/m/bluewave-labs/checkmate)
![](https://img.shields.io/github/last-commit/bluewave-labs/checkmate)
![](https://img.shields.io/github/languages/top/bluewave-labs/checkmate)
![](https://img.shields.io/github/issues/bluewave-labs/checkmate)
![](https://img.shields.io/github/issues-pr/bluewave-labs/checkmate)
[![OpenSSF Mejores Prácticas](https://www.bestpractices.dev/projects/9901/badge)](https://www.bestpractices.dev/projects/9901)
[![Pregunta en DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bluewave-labs/checkmate)
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
<p align="center"><strong>Una aplicación de código abierto para monitoreo de infraestructura y tiempo de actividad</strong></p>
<img width="1660" alt="image" src="https://github.com/user-attachments/assets/b748f36d-a271-4965-ad0a-18bf153bbee7" />
Este repositorio contiene tanto el frontend como el backend de Checkmate, una herramienta de monitoreo de código abierto y autoalojada para rastrear hardware de servidores, tiempo de actividad, tiempos de respuesta e incidentes en tiempo real con visualizaciones atractivas. Checkmate revisa regularmente si un servidor o sitio web es accesible y funciona de manera óptima, proporcionando alertas e informes en tiempo real sobre la disponibilidad, el tiempo de inactividad y el tiempo de respuesta de los servicios monitoreados.
Checkmate también tiene un agente llamado [Capture](https://github.com/bluewave-labs/capture), para recuperar datos de servidores remotos. Aunque Capture no es obligatorio para ejecutar Checkmate, proporciona información adicional sobre el estado de la CPU, RAM, disco y temperatura de tus servidores.
Checkmate ha sido probado con más de 1000 monitores activos sin problemas ni cuellos de botella de rendimiento.
**Si deseas patrocinar una función, [visita este enlace](https://checkmate.so/sponsored-features).**
## 📚 Tabla de contenidos
- [📦 Demo](#demo)
- [🔗 Guía del usuario](#guía-del-usuario)
- [🛠️ Instalación](#instalación)
- [🏁 Traducciones](#traducciones)
- [🚀 Rendimiento](#rendimiento)
- [💚 Preguntas e ideas](#preguntas-e-ideas)
- [🧩 Características](#características)
- [🏗️ Capturas de pantalla](#capturas-de-pantalla)
- [🏗️ Tecnologías](#tecnologías)
- [🔗 Enlaces útiles](#enlaces-útiles)
- [🤝 Contribuciones](#contribuciones)
- [💰 Patrocinadores](#patrocinadores)
...
**[Texto truncado para mantener la longitud del mensaje manejable]**
## Demo
Puedes ver la última versión de [Checkmate](https://checkmate-demo.bluewavelabs.ca/) en acción. El usuario es uptimedemo@demo.com y la contraseña es Demouser1! (ten en cuenta que actualizamos el servidor de demostración de vez en cuando, así que si no funciona para ti, por favor contáctanos en el canal de Discusiones).
## Guía del usuario
Las instrucciones de uso se pueden encontrar [aquí](https://docs.checkmate.so/checkmate-2.1). Todavía está en desarrollo y parte de la información puede estar desactualizada ya que continuamente añadimos funciones cada semana. ¡Ten por seguro que estamos haciendo lo mejor posible! :)
## Instalación
Consulta las instrucciones de instalación en el [portal de documentación de Checkmate](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart).
Alternativamente, también puedes usar [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md) o [Pikapods](https://www.pikapods.com/) para desplegar rápidamente una instancia de Checkmate. Si deseas monitorear tu infraestructura de servidores, necesitarás el agente [Capture](https://github.com/bluewave-labs/capture). El repositorio de Capture también contiene las instrucciones de instalación.
## Traducciones
Si deseas usar Checkmate en tu idioma, por favor [ve a esta página](https://poeditor.com/join/project/lRUoGZFCsJ) y regístrate para el idioma al que te gustaría traducir Checkmate.
## Rendimiento
Gracias a extensas optimizaciones, Checkmate opera con un uso de memoria excepcionalmente bajo, requiriendo recursos mínimos de memoria y CPU. Aquí está el uso de memoria de una instancia de Node.js ejecutándose en un servidor que monitorea 323 servidores cada minuto:
![image](https://github.com/user-attachments/assets/37e04a75-d83a-488f-b25c-025511b492c9)
También puedes ver el consumo de memoria de MongoDB y Redis en el mismo servidor (398Mb y 15Mb) para la misma cantidad de servidores:
![image](https://github.com/user-attachments/assets/3b469e85-e675-4040-a162-3f24c1afc751)
## Preguntas e Ideas
Si tienes alguna pregunta, sugerencia o comentario, tienes varias opciones:
- [Canal de Discord](https://discord.gg/NAb6H3UTjK)
- [Discusiones en GitHub](https://github.com/bluewave-labs/bluewave-uptime/discussions)
- [Grupo en Reddit](https://www.reddit.com/r/CheckmateMonitoring/)
¡No dudes en hacer preguntas o compartir tus ideas, nos encantaría saber de ti!
## Características
- Completamente de código abierto, desplegable en tus propios servidores o dispositivos personales (por ejemplo, Raspberry Pi 4 o 5)
- Monitoreo de sitios web
- Monitoreo de velocidad de carga
- Monitoreo de infraestructura (memoria, uso de disco, rendimiento de CPU, etc) - requiere el agente [Capture](https://github.com/bluewave-labs/capture)
- Monitoreo de Docker
- Monitoreo con ping
- Monitoreo de certificados SSL
- Monitoreo de puertos
- Incidentes en una sola vista
- Páginas de estado
- Notificaciones por correo, Webhooks, Discord, Telegram, Slack
- Mantenimiento programado
- Monitoreo de consultas JSON
- Soporte para múltiples idiomas
**Hoja de ruta a corto plazo:** ([Milestone 2.2](https://github.com/bluewave-labs/Checkmate/milestone/8))
- Mejores notificaciones
- Monitoreo de red
- ...y algunas funciones más
## Capturas de pantalla
<p>
<img width="1628" alt="image" src="https://github.com/user-attachments/assets/2eff6464-0738-4a32-9312-26e1e8e86275" />
</p>
<p>
<img width="1656" alt="image" src="https://github.com/user-attachments/assets/616c3563-c2a7-4ee4-af6c-7e6068955d1a" />
</p>
<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>
## Tecnologías
- [ReactJs](https://react.dev/)
- [MUI (framework de React)](https://mui.com/)
- [Node.js](https://nodejs.org/en)
- [MongoDB](https://mongodb.com)
- [Recharts](https://recharts.org)
- ¡Y muchos otros componentes de código abierto!
## Enlaces útiles
- Si deseas apoyarnos, por favor considera darle una ⭐ y haz clic en "watch".
- ¿Tienes una pregunta o sugerencia para la hoja de ruta? Revisa nuestro [canal de Discord](https://discord.gg/NAb6H3UTjK) o el foro de [Discusiones](https://github.com/bluewave-labs/checkmate/discussions).
- ¿Quieres saber cuándo hay una nueva versión? Usa [Newreleases](https://newreleases.io/), un servicio gratuito para seguir lanzamientos.
- Mira un video de instalación y uso de Checkmate [aquí](https://www.youtube.com/watch?v=GfFOc0xHIwY)
## Contribuciones
Somos [Alex](http://github.com/ajhollid) (líder de equipo), [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) y [Mert](https://github.com/mertssmnoglu), ayudando a personas y empresas a monitorear su infraestructura y servidores.
Nos enorgullecemos de construir conexiones fuertes con contribuyentes de todos los niveles. A pesar de ser un proyecto joven, Checkmate ya ha ganado más de 7000 estrellas y ha atraído a más de 90 contribuyentes de todo el mundo.
Nuestro repositorio ha sido marcado con estrella por empleados de **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes y NEC**, ¡así que no te detengas — participa, contribuye y aprende con nosotros!
Cómo contribuir:
0. Dale una estrella al repositorio :)
1. Revisa la [guía para contribuidores](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). Se anima a los nuevos a revisar las etiquetas `good-first-issue`.
2. Consulta la [estructura del proyecto](https://docs.checkmate.so/checkmate-2.1/developers-guide/general-project-structure) y la [visión general](https://bluewavelabs.gitbook.io/checkmate/developers-guide/high-level-overview).
3. Lee una estructura detallada de [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) si deseas profundizar en la arquitectura.
4. Abre un issue si crees que has encontrado un error.
5. Revisa los issues con la etiqueta `good-first-issue` si eres nuevo.
6. Haz un pull request para añadir nuevas funciones, mejoras o correcciones.
<a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
</a>
[![Historial de estrellas](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
## Patrocinadores
Gracias a [Gitbook](https://gitbook.io/) por darnos una cuenta gratuita para su plataforma de documentación, y a [Poeditor](https://poeditor.com/) por proporcionarnos una cuenta gratuita para servicios de traducción. Si deseas patrocinar Checkmate, por favor envía un correo a hello@bluewavelabs.ca
Si deseas patrocinar una función, [visita esta página](https://checkmate.so/sponsored-features).
También puedes revisar otros proyectos de BlueWave orientados a desarrolladores y contribuidores:
- [VerifyWise](https://github.com/bluewave-labs/verifywise), la primera plataforma de gobernanza de IA de código abierto.
- [DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), una aplicación de intercambio de archivos seguro, también conocida como dataroom.
- [Guidefox](https://github.com/bluewave-labs/guidefox), una app que ayuda a nuevos usuarios a aprender a usar tu producto mediante pistas, tours, popups y banners.
+47 -32
View File
@@ -18,7 +18,7 @@
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.
Checkmate also has an agent, called [Capture](https://github.com/bluewave-labs/capture), to retrieve data from remote servers. While Capture is not required to run Checkmate, it provides additional insights about your servers' CPU, RAM, disk, and temperature status.
Checkmate also has an agent, called [Capture](https://github.com/bluewave-labs/capture), to retrieve data from remote servers. While Capture is not required to run Checkmate, it provides additional insights about your servers' CPU, RAM, disk, and temperature status. Capture can run on Linux, Windows, Mac, Raspberry Pi, or any device that can run Go.
Checkmate has been stress-tested with 1000+ active monitors without any particular issues or performance bottlenecks.
@@ -26,41 +26,46 @@ Checkmate has been stress-tested with 1000+ active monitors without any particul
## 📚 Table of contents
- [📦 Demo](#-demo)
- [🔗 User's guide](#-users-guide)
- [🛠️ Installation](#️-installation)
- [🚀 Deploying Checkmate with Helm](#-deploying-checkmate-with-helm)
- [🏁 Translations](#-translations)
- [🚀 Performance](#-performance)
- [💚 Questions & Ideas](#-questions--ideas)
- [🧩 Features](#-features)
- [🏗️ Screenshots](#-screenshots)
- [🏗️ Tech stack](#-tech-stack)
- [📦 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)
- [🤝 Contributing](#contributing)
- [💰 Our sponsors](#our-sponsors)
## 📦 Demo
## Demo
You can see the latest build of [Checkmate](https://checkmate-demo.bluewavelabs.ca/) in action. The username is uptimedemo@demo.com and the password is Demouser1! (just a note that we update the demo server from time to time, so if it doesn't work for you, please ping us on the Discussions channel).
## 🔗 User's guide
## User's guide
Usage instructions can be found [here](https://docs.checkmate.so/checkmate-2.1). It's still WIP and some of the information there might be outdated as we continuously add features weekly. Rest assured, we are doing our best! :)
## 🛠️ Installation
## 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/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md) 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.
Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa) 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.
### Using a Custom CA
If you need to monitor internal HTTPS endpoints with certificates from private Certificate Authorities (like Smallstep), see our [Custom CA Trust Guide](./docs/custom-ca-trust.md) for Docker configuration options.
For more documentation, see the [docs directory](./docs/).
## 🏁 Translations
## Translations
If you would like to use Checkmate in your language, please [go to this page](https://poeditor.com/join/project/lRUoGZFCsJ) and register for the language you would like to translate Checkmate to.
## 🚀 Performance
## Performance
Thanks to extensive optimizations, Checkmate operates with an exceptionally small memory footprint, requiring minimal memory and CPU resources. Here’s the memory usage of a Node.js instance running on a server that monitors 323 servers every minute:
@@ -70,34 +75,44 @@ You can see the memory footprint of MongoDB and Redis on the same server (398Mb
![image](https://github.com/user-attachments/assets/3b469e85-e675-4040-a162-3f24c1afc751)
## 💚 Questions & Ideas
## Questions & Ideas
If you have any questions, suggestions or comments, please use our [Discord channel](https://discord.gg/NAb6H3UTjK). We've also launched our [Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions) page! Feel free to ask questions or share your ideas—we'd love to hear from you!
If you have any questions, suggestions or comments, you have several options:
## 🧩 Features
- [Discord channel](https://discord.gg/NAb6H3UTjK)
- [GitHub Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions)
- [Reddit group](https://www.reddit.com/r/CheckmateMonitoring/)
Feel free to ask questions or share your ideas - we'd love to hear from you!
## Features
- Completely open source, deployable on your servers or home devices (e.g Raspberry Pi 4 or 5)
- Website monitoring
- Page speed monitoring
- Infrastructure monitoring (memory, disk usage, CPU performance etc) - requires [Capture](https://github.com/bluewave-labs/capture)
- Infrastructure monitoring (memory, disk usage, CPU performance, network etc) - requires [Capture](https://github.com/bluewave-labs/capture) agent
- Docker monitoring
- Ping monitoring
- SSL monitoring
- Port monitoring
- Game server monitoring (3.0)
- Incidents at a glance
- Status pages
- E-mail, Webhooks, Discord, Telegram, Slack notifications
- E-mail, Webhooks, Discord and Slack notifications
- Scheduled maintenance
- JSON query monitoring
- Support for multiple languages
- Multi-language support for English, German, Japanese, Portuguese (Brazil), Russian, Turkish, Ukrainian, Vietnamese, Chinese (Traditional, Taiwan)
**Short term roadmap:** ([Milestone 2.2](https://github.com/bluewave-labs/Checkmate/milestone/8))
**Short term roadmap:**
- Plugins that will help Checkmate get any information from a remote service (e.g database, etc)
- Better notifications
- Network monitoring
- ..and a few more features
## 🏗️ Screenshots
If you would like to sponsor an additional feature, [see this page](https://checkmate.so/sponsored-features).
## Screenshots
<p>
<img width="1628" alt="image" src="https://github.com/user-attachments/assets/2eff6464-0738-4a32-9312-26e1e8e86275" />
@@ -114,7 +129,7 @@ If you have any questions, suggestions or comments, please use our [Discord chan
## 🏗️ Tech stack
## Tech stack
- [ReactJs](https://react.dev/)
- [MUI (React framework)](https://mui.com/)
@@ -130,11 +145,11 @@ If you have any questions, suggestions or comments, please use our [Discord chan
- Need a ping when there's a new release? Use [Newreleases](https://newreleases.io/), a free service to track releases.
- Watch a Checkmate [installation and usage video](https://www.youtube.com/watch?v=GfFOc0xHIwY)
## 🤝 Contributing
## Contributing
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 are [Alex](http://github.com/ajhollid) (team lead), [Gorkem](http://github.com/gorkem-bwl/), [Owaise](http://github.com/Owaiseimdad), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) and [Karen](https://github.com/karenvicent) 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 6000+ stars and attracted 80+ 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 7000+ stars and attracted 90+ 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!
@@ -154,7 +169,7 @@ Here's how you can contribute:
[![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
## 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
+1 -1
View File
@@ -14,6 +14,6 @@ metadata:
name: checkmate-secrets
type: Opaque
stringData:
{{- range $key, $value := := $secrets }}
{{- range $key, $value := $secrets }}
{{ $key }}: {{ $value | quote }}
{{- end }}
@@ -20,11 +20,3 @@ spec:
envFrom:
- secretRef:
name: checkmate-secrets
volumeMounts:
- name: docker-sock
mountPath: /var/run/docker.sock
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock
type: Socket
@@ -5,6 +5,11 @@ metadata:
name: server-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "http://{{ .Values.client.ingress.host }},https://{{ .Values.client.ingress.host }}"
nginx.ingress.kubernetes.io/cors-allow-methods: "GET, PUT, POST, DELETE, PATCH, OPTIONS"
nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization"
nginx.ingress.kubernetes.io/cors-allow-credentials: "true"
spec:
rules:
- host: {{ .Values.server.ingress.host }}
+20 -627
View File
@@ -21,7 +21,7 @@
"dayjs": "1.11.13",
"flag-icons": "7.3.2",
"html2canvas": "^1.4.1",
"i18next": "^24.2.2",
"i18next": "25.4.2",
"joi": "17.13.3",
"mui-color-input": "^6.0.0",
"react": "18.3.1",
@@ -45,10 +45,7 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.6",
"prettier": "^3.3.3",
"vite": "6.3.5"
},
"optionalDependencies": {
"@rollup/rollup-linux-arm64-musl": "4.41.0"
"vite": "6.3.6"
}
},
"node_modules/@ampproject/remapping": {
@@ -283,9 +280,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz",
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz",
"integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -491,262 +488,6 @@
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
"integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
"integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
"integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
"integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
"integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
"integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
"integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
"integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
"integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
"integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
"integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
"integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
"integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
"cpu": [
"mips64el"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
"integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
"integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
"integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
@@ -763,134 +504,6 @@
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
"integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
"integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
"integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
"integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
"integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
"integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
"integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
"integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
@@ -2063,123 +1676,6 @@
}
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz",
"integrity": "sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.0.tgz",
"integrity": "sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.0.tgz",
"integrity": "sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.0.tgz",
"integrity": "sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.0.tgz",
"integrity": "sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.0.tgz",
"integrity": "sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.0.tgz",
"integrity": "sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.0.tgz",
"integrity": "sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.0.tgz",
"integrity": "sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz",
@@ -2193,71 +1689,6 @@
"linux"
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.0.tgz",
"integrity": "sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.0.tgz",
"integrity": "sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.0.tgz",
"integrity": "sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.0.tgz",
"integrity": "sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.0.tgz",
"integrity": "sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz",
@@ -2284,45 +1715,6 @@
"linux"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.0.tgz",
"integrity": "sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.0.tgz",
"integrity": "sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.0.tgz",
"integrity": "sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@sideway/address": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
@@ -3030,13 +2422,13 @@
}
},
"node_modules/axios": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@@ -4342,14 +3734,15 @@
}
},
"node_modules/form-data": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
@@ -4690,9 +4083,9 @@
}
},
"node_modules/i18next": {
"version": "24.2.3",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.3.tgz",
"integrity": "sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==",
"version": "25.4.2",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.4.2.tgz",
"integrity": "sha512-gD4T25a6ovNXsfXY1TwHXXXLnD/K2t99jyYMCSimSCBnBRJVQr5j+VAaU83RJCPzrTGhVQ6dqIga66xO2rtd5g==",
"funding": [
{
"type": "individual",
@@ -4709,7 +4102,7 @@
],
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.10"
"@babel/runtime": "^7.27.6"
},
"peerDependencies": {
"typescript": "^5"
@@ -6996,9 +6389,9 @@
}
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"version": "6.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
+2 -5
View File
@@ -26,7 +26,7 @@
"dayjs": "1.11.13",
"flag-icons": "7.3.2",
"html2canvas": "^1.4.1",
"i18next": "^24.2.2",
"i18next": "25.4.2",
"joi": "17.13.3",
"mui-color-input": "^6.0.0",
"react": "18.3.1",
@@ -58,9 +58,6 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.6",
"prettier": "^3.3.3",
"vite": "6.3.5"
},
"optionalDependencies": {
"@rollup/rollup-linux-arm64-musl": "4.41.0"
"vite": "6.3.6"
}
}
+35
View File
@@ -0,0 +1,35 @@
import LeftArrow from "../../assets/icons/left-arrow.svg?react";
import LeftArrowDouble from "../../assets/icons/left-arrow-double.svg?react";
import LeftArrowLong from "../../assets/icons/left-arrow-long.svg?react";
import PropTypes from "prop-types";
const ArrowLeft = ({ type, color = "#667085", ...props }) => {
if (type === "double") {
return (
<LeftArrowDouble
style={{ color }}
{...props}
/>
);
} else if (type === "long") {
return (
<LeftArrowLong
style={{ color }}
{...props}
/>
);
} else {
return (
<LeftArrow
style={{ color }}
{...props}
/>
);
}
};
ArrowLeft.propTypes = {
color: PropTypes.string,
type: PropTypes.oneOf(["double", "long", "default"]),
};
export default ArrowLeft;
@@ -0,0 +1,28 @@
import RightArrow from "../../assets/icons/right-arrow.svg?react";
import RightArrowDouble from "../../assets/icons/right-arrow-double.svg?react";
import PropTypes from "prop-types";
const ArrowRight = ({ type, color = "#667085", ...props }) => {
if (type === "double") {
return (
<RightArrowDouble
style={{ color }}
{...props}
/>
);
} else {
return (
<RightArrow
style={{ color }}
{...props}
/>
);
}
};
ArrowRight.propTypes = {
type: PropTypes.oneOf(["double", "default"]),
color: PropTypes.string,
};
export default ArrowRight;
+3 -1
View File
@@ -15,7 +15,7 @@ import { useTheme } from "@emotion/react";
* <Avatar src="assets/img" first="Alex" last="Holliday" small />
*/
const Avatar = ({ src, small, sx }) => {
const Avatar = ({ src, small, sx, onClick = () => {} }) => {
const { user } = useSelector((state) => state.auth);
const theme = useTheme();
@@ -31,6 +31,7 @@ const Avatar = ({ src, small, sx }) => {
return (
<MuiAvatar
onClick={onClick}
alt={`${user?.firstName} ${user?.lastName}`}
/* TODO What is the /static/images/avatar/2.jpg ?*/
src={src ? src : user?.avatarImage ? image : "/static/images/avatar/2.jpg"}
@@ -66,6 +67,7 @@ Avatar.propTypes = {
src: PropTypes.string,
small: PropTypes.bool,
sx: PropTypes.object,
onClick: PropTypes.func,
};
export default Avatar;
+1 -2
View File
@@ -2,8 +2,7 @@ import PropTypes from "prop-types";
import { Box, Breadcrumbs as MUIBreadcrumbs } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import ArrowRight from "../../assets/icons/right-arrow.svg?react";
import ArrowRight from "../ArrowRight";
import "./index.css";
/**
@@ -1,8 +1,10 @@
import { useTheme } from "@emotion/react";
import { useEffect, useState, useMemo } from "react";
import Stack from "@mui/material/Stack";
import { Box, Typography } from "@mui/material";
import PropTypes from "prop-types";
import "./index.css";
import CircularProgress from "@mui/material/CircularProgress";
const MINIMUM_VALUE = 0;
const MAXIMUM_VALUE = 100;
@@ -15,19 +17,26 @@ const MAXIMUM_VALUE = 100;
* @param {number} [props.progress=0] - Progress percentage (0-100)
* @param {number} [props.radius=60] - Radius of the gauge circle
* @param {number} [props.strokeWidth=15] - Width of the gauge stroke
* @param {number} [props.threshold=50] - Threshold for color change
* @param {number} [props.precision=1] - Precision of the progress percentage
* @param {string} [props.unit="%"] - Unit of progress
*
* @example
* <CustomGauge
* progress={75}
* radius={50}
* strokeWidth={10}
* threshold={50}
* />
*
* @returns {React.ReactElement} Rendered CustomGauge component
*/
const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold = 50 }) => {
const CustomGauge = ({
isLoading = false,
progress = 0,
radius = 70,
strokeWidth = 15,
precision = 1,
unit = "%",
}) => {
const theme = useTheme();
// Calculate the length of the stroke for the circle
const { circumference, totalSize, strokeLength } = useMemo(
@@ -52,10 +61,28 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold =
const progressWithinRange = Math.max(MINIMUM_VALUE, Math.min(progress, MAXIMUM_VALUE));
const fillColor =
progressWithinRange > threshold
? theme.palette.error.lowContrast // CAIO_REVIEW
: theme.palette.accent.main; // CAIO_REVIEW
let fillColor;
if (progressWithinRange < 50) {
fillColor = theme.palette.success.main;
} else if (progressWithinRange < 80) {
fillColor = theme.palette.warning.lowContrast;
} else {
fillColor = theme.palette.error.lowContrast;
}
if (isLoading) {
return (
<Stack
className="radial-chart"
width={radius}
height={radius}
alignItems="center"
justifyContent="center"
>
<CircularProgress color="accent" />
</Stack>
);
}
return (
<Box
@@ -71,7 +98,7 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold =
>
<circle
className="radial-chart-base"
stroke={theme.palette.secondary.light} // CAIO_REVIEW
stroke={theme.palette.secondary.light}
strokeWidth={strokeWidth}
fill="none"
cx={totalSize / 2} // Center the circle
@@ -103,7 +130,7 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold =
fill: theme.typography.h2.color,
}}
>
{`${progressWithinRange.toFixed(1)}%`}
{`${progressWithinRange.toFixed(precision)}${unit}`}
</Typography>
</Box>
);
@@ -112,8 +139,10 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold =
export default CustomGauge;
CustomGauge.propTypes = {
isLoading: PropTypes.bool,
progress: PropTypes.number,
radius: PropTypes.number,
strokeWidth: PropTypes.number,
threshold: PropTypes.number,
precision: PropTypes.number,
unit: PropTypes.string,
};
@@ -1,7 +1,7 @@
import { useTheme } from "@emotion/react";
import { Box, Stack, Tooltip, Typography } from "@mui/material";
import { formatDateWithTz } from "../../../Utils/timeUtils";
import { useEffect, useState } from "react";
import { useEffect, useState, forwardRef } from "react";
import { useSelector } from "react-redux";
import PropTypes from "prop-types";
@@ -17,26 +17,32 @@ import PropTypes from "prop-types";
* @returns {JSX.Element} The Bar component.
*/
const Bar = ({ width, height, backgroundColor, borderRadius, children }) => {
const theme = useTheme();
const Bar = forwardRef(
({ width, height, backgroundColor, borderRadius, children, ...otherProps }, ref) => {
const theme = useTheme();
return (
<Box
position="relative"
width={width}
height={height}
backgroundColor={backgroundColor}
sx={{
borderRadius: borderRadius || theme.spacing(1.5),
}}
>
{children}
</Box>
);
};
return (
<Box
ref={ref}
position="relative"
width={width}
height={height}
backgroundColor={backgroundColor}
sx={{
borderRadius: borderRadius || theme.spacing(1.5),
}}
{...otherProps}
>
{children}
</Box>
);
}
);
Bar.displayName = "Bar";
Bar.propTypes = {
width: PropTypes.string.isRequired,
width: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
height: PropTypes.string.isRequired,
backgroundColor: PropTypes.string.isRequired,
borderRadius: PropTypes.string,
@@ -1,5 +1,6 @@
export const tooltipDateFormatLookup = (dateRange) => {
const dateFormatLookup = {
recent: "ddd. MMMM D, YYYY, hh:mm A",
day: "ddd. MMMM D, YYYY, hh:mm A",
week: "ddd. MMMM D, YYYY, hh:mm A",
month: "ddd. MMMM D, YYYY",
@@ -88,7 +88,51 @@ PercentTick.propTypes = {
*/
const getFormattedPercentage = (value) => {
if (typeof value !== "number") return value;
return `${(value * 100).toFixed(2)}.%`;
return `${(value * 100).toFixed(2)}%`;
};
/**
* Custom tick component for rendering network bytes per second.
*
* @param {Object} props - The properties object.
* @param {number} props.x - The x-coordinate for the tick.
* @param {number} props.y - The y-coordinate for the tick.
* @param {Object} props.payload - The payload object containing tick data.
* @param {number} props.index - The index of the tick.
* @returns {JSX.Element|null} The rendered tick component or null for the first tick.
*/
export const NetworkTick = ({ x, y, payload, index, formatter }) => {
const theme = useTheme();
if (index === 0) return null;
if (formatter === undefined) {
formatter = (value, space = false) => {
if (typeof value !== "number") return value;
// need to add space between value and unit
return `${(value / 1024).toFixed(1)}${space ? " " : ""}Kbps`;
};
}
return (
<Text
x={x - 20}
y={y}
textAnchor="middle"
fill={theme.palette.primary.contrastTextTertiary}
fontSize={11}
fontWeight={400}
>
{formatter(payload?.value, true)}
</Text>
);
};
NetworkTick.propTypes = {
x: PropTypes.number,
y: PropTypes.number,
payload: PropTypes.object,
index: PropTypes.number,
formatter: PropTypes.func,
};
/**
@@ -112,6 +156,7 @@ export const InfrastructureTooltip = ({
yLabel,
dotColor,
dateRange,
formatter = getFormattedPercentage,
}) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
const theme = useTheme();
@@ -166,8 +211,8 @@ export const InfrastructureTooltip = ({
sx={{ opacity: 0.8 }}
>
{yIdx >= 0
? `${yLabel} ${getFormattedPercentage(payload[0].payload[hardwareType][yIdx][metric])}`
: `${yLabel} ${getFormattedPercentage(payload[0].payload[yKey])}`}
? `${yLabel} ${formatter(payload[0].payload[hardwareType][yIdx][metric])}`
: `${yLabel} ${formatter(payload[0].payload[yKey])}`}
</Typography>
</Stack>
</Box>
+4 -3
View File
@@ -51,9 +51,10 @@ const Check = ({ text, noHighlightText, variant = "info", outlined = false }) =>
sx={{
color:
variant === "info"
? theme.palette.primary.contrastTextTertiary
? theme.palette.primary.contrastTextSecondary
: colors[variant],
opacity: 0.8,
opacity: 0.9,
fontWeight: 450,
}}
>
{noHighlightText && <Typography component="span">{noHighlightText}</Typography>}{" "}
@@ -64,7 +65,7 @@ const Check = ({ text, noHighlightText, variant = "info", outlined = false }) =>
};
Check.propTypes = {
text: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
text: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
noHighlightText: PropTypes.string,
variant: PropTypes.oneOf(["info", "error", "success"]),
outlined: PropTypes.bool,
+8 -4
View File
@@ -4,15 +4,14 @@ const ConfigBox = styled(Stack)(({ theme }) => ({
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
gap: theme.spacing(20),
backgroundColor: theme.palette.primary.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.spacing(2),
"& > *": {
paddingTop: theme.spacing(12),
paddingBottom: theme.spacing(18),
paddingTop: theme.spacing(15),
paddingBottom: theme.spacing(15),
},
"& > div:first-of-type": {
flex: 0.7,
@@ -21,11 +20,16 @@ const ConfigBox = styled(Stack)(({ theme }) => ({
borderRightColor: theme.palette.primary.lowContrast,
paddingRight: theme.spacing(15),
paddingLeft: theme.spacing(15),
backgroundColor: theme.palette.tertiary.background,
"& :is(h1, h2):first-of-type": {
fontWeight: 600,
marginBottom: theme.spacing(4),
},
},
"& > div:last-of-type": {
flex: 1,
paddingRight: theme.spacing(20),
paddingLeft: theme.spacing(18),
paddingLeft: theme.spacing(20),
},
"& h1, & h2": {
color: theme.palette.primary.contrastTextSecondary,
@@ -2,7 +2,7 @@ import { useId } from "react";
import PropTypes from "prop-types";
import { Modal, Stack, Typography } from "@mui/material";
const GenericDialog = ({ title, description, open, onClose, theme, children }) => {
const GenericDialog = ({ title, description, open, onClose, theme, children, width }) => {
const titleId = useId();
const descriptionId = useId();
const ariaDescribedBy = description?.length > 0 ? descriptionId : "";
@@ -16,6 +16,7 @@ const GenericDialog = ({ title, description, open, onClose, theme, children }) =
>
<Stack
gap={theme.spacing(2)}
width={width}
sx={{
position: "absolute",
top: "50%",
@@ -39,6 +40,7 @@ const GenericDialog = ({ title, description, open, onClose, theme, children }) =
fontSize={16}
color={theme.palette.primary.contrastText}
fontWeight={600}
marginBottom={theme.spacing(4)}
>
{title}
</Typography>
@@ -46,6 +48,7 @@ const GenericDialog = ({ title, description, open, onClose, theme, children }) =
<Typography
id={descriptionId}
color={theme.palette.primary.contrastTextTertiary}
marginBottom={theme.spacing(4)}
>
{description}
</Typography>
@@ -64,6 +67,7 @@ GenericDialog.propTypes = {
theme: PropTypes.object.isRequired,
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node])
.isRequired,
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]),
};
export { GenericDialog };
@@ -0,0 +1,44 @@
import { Button, Stack } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
const FallbackActionButtons = ({ link, type }) => {
const theme = useTheme();
const navigate = useNavigate();
const { t } = useTranslation();
return (
<Stack
gap={theme.spacing(10)}
alignItems="center"
>
<Button
variant="contained"
color="accent"
sx={{ fontWeight: 700 }}
onClick={() => navigate(link)}
>
{t(`${type}.fallback.actionButton`)}
</Button>
{type === "uptimeMonitor" && (
<Button
variant="contained"
color="accent"
sx={{ alignSelf: "center" }}
onClick={() => navigate("/uptime/bulk-import")}
>
{t("bulkImport.fallbackPage")}
</Button>
)}
</Stack>
);
};
FallbackActionButtons.propTypes = {
title: PropTypes.string.isRequired,
link: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
};
export default FallbackActionButtons;
@@ -0,0 +1,39 @@
import { useTheme } from "@emotion/react";
import Box from "@mui/material/Box";
import Background from "../../assets/Images/background-grid.svg?react";
import OutputAnimation from "../../assets/Animations/output.gif";
import DarkmodeOutput from "../../assets/Animations/darkmodeOutput.gif";
import { useSelector } from "react-redux";
const FallbackBackground = () => {
const theme = useTheme();
const mode = useSelector((state) => state.ui.mode);
return (
<>
<Box
component="img"
src={mode === "light" ? OutputAnimation : DarkmodeOutput}
background="transparent"
alt="Loading animation"
sx={{
zIndex: 1,
border: "none",
borderRadius: theme.spacing(8),
width: "100%",
transform: "scale(0.6667)",
}}
/>
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
</>
);
};
export default FallbackBackground;
@@ -0,0 +1,35 @@
import PropTypes from "prop-types";
import { useTheme } from "@mui/material/styles";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Check from "../Check/Check";
const FallbackCheckList = ({ checks, type }) => {
const theme = useTheme();
return (
<Box
sx={{
display: "flex",
flexWrap: "wrap",
gap: theme.spacing(2),
alignItems: "flex-start",
maxWidth: { xs: "90%", md: "80%", lg: "75%" },
}}
>
{checks?.map((check, index) => (
<Check
text={check}
key={`${type.trim().split(" ")[0]}-${index}`}
outlined={true}
/>
))}
</Box>
);
};
FallbackCheckList.propTypes = {
checks: PropTypes.arrayOf(PropTypes.string).isRequired,
title: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
};
export default FallbackCheckList;
@@ -0,0 +1,44 @@
import { useTheme } from "@emotion/react";
import { Box, Stack } from "@mui/material";
import PropTypes from "prop-types";
const FallbackContainer = ({ children, type }) => {
const theme = useTheme();
return (
<Box
border={1}
borderColor={theme.palette.tertiary.border}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.tertiary.background}
overflow="hidden"
sx={{
display: "flex",
borderStyle: "dashed",
height: "fit-content",
minHeight: "60vh",
width: {
sm: "90%",
md: "70%",
lg: "50%",
xl: "40%",
},
padding: `${theme.spacing(20)} ${theme.spacing(10)}`,
}}
>
<Stack
className={`fallback__${type?.trim().split(" ")[0]}`}
alignItems="center"
gap={theme.spacing(20)}
>
{children}
</Stack>
</Box>
);
};
FallbackContainer.propTypes = {
children: PropTypes.node,
type: PropTypes.string,
};
export default FallbackContainer;
@@ -0,0 +1,69 @@
import Box from "@mui/material/Box";
import { useTheme } from "@mui/material/styles";
import Link from "@mui/material/Link";
import { Link as RouterLink } from "react-router-dom";
import { useTranslation } from "react-i18next";
import Alert from "../Alert";
import PropTypes from "prop-types";
const renderWarningMessage = (t) => {
return (
<>
{t("pageSpeedWarning")}{" "}
<Link
component={RouterLink}
to="/settings"
sx={{
textDecoration: "underline",
color: "inherit",
fontWeight: "inherit",
"&:hover": {
textDecoration: "underline",
},
}}
>
{t("pageSpeedLearnMoreLink")}
</Link>{" "}
{t("pageSpeedAddApiKey")}
</>
);
};
const FallbackPageSpeedWarning = ({ settingsData }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<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(t)}
/>
)}
</Box>
</Box>
);
};
FallbackPageSpeedWarning.propTypes = {
settingsData: PropTypes.shape({
pagespeedKeySet: PropTypes.bool,
}),
};
export default FallbackPageSpeedWarning;
@@ -0,0 +1,21 @@
import { useTheme } from "@mui/material/styles";
import { Typography } from "@mui/material";
import PropTypes from "prop-types";
const FallbackTitle = ({ title }) => {
const theme = useTheme();
return (
<Typography
alignSelf="center"
component="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastText}
>
{title}
</Typography>
);
};
FallbackTitle.propTypes = {
title: PropTypes.string.isRequired,
};
export default FallbackTitle;
-4
View File
@@ -38,7 +38,3 @@
background-size: cover;
background-repeat: no-repeat;
}
.fallback__status > .MuiStack-root {
margin-left: var(--env-var-spacing-2);
}
+34 -174
View File
@@ -1,72 +1,26 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Button, Stack, Typography, Link } from "@mui/material";
import { Link as RouterLink } from "react-router-dom";
import Skeleton from "../../assets/Images/create-placeholder.svg?react";
import SkeletonDark from "../../assets/Images/create-placeholder-dark.svg?react";
import Background from "../../assets/Images/background-grid.svg?react";
import Check from "../Check/Check";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import Alert from "../Alert";
import { useTranslation } from "react-i18next";
import { Box, Stack } from "@mui/material";
import "./index.css";
import { useFetchSettings } from "../../Hooks/settingsHooks";
import { useState } from "react";
import FallbackTitle from "./FallbackTitle";
import FallbackCheckList from "./FallbackCheckList";
import FallbackActionButtons from "./FallBackActionButtons";
import FallbackBackground from "./FallbackBackground";
import FallbackContainer from "./FallbackContainer";
/**
* Fallback component to display a fallback UI with a title, a list of checks, and a navigation button.
*
* @param {Object} props - The component props.
* @param {string} props.title - The title to be displayed in the fallback UI.
* @param {string} props.type - The type of the fallback (e.g., "pageSpeed", "notifications").
* @param {Array<string>} props.checks - An array of strings representing the checks to display.
* @param {string} [props.link="/"] - The link to navigate to.
* @param {boolean} [props.vowelStart=false] - Whether the title starts with a vowel.
* @param {boolean} [props.showPageSpeedWarning=false] - Whether to show the PageSpeed API warning.
* @returns {JSX.Element} The rendered fallback UI.
*/
const Fallback = ({
title,
checks,
link = "/",
isAdmin,
vowelStart = false,
showPageSpeedWarning = false,
}) => {
const Fallback = ({ title, checks, link = "/", isAdmin, type, children }) => {
const theme = useTheme();
const navigate = useNavigate();
const mode = useSelector((state) => state.ui.mode);
const { t } = useTranslation();
const [settingsData, setSettingsData] = useState(undefined);
const [isLoading, error] = useFetchSettings({
setSettingsData,
setIsApiKeySet: () => {},
setIsEmailPasswordSet: () => {},
});
// Custom warning message with clickable link
const renderWarningMessage = () => {
return (
<>
{t("pageSpeedWarning")}{" "}
<Link
component={RouterLink}
to="/settings"
sx={{
textDecoration: "underline",
color: "inherit",
fontWeight: "inherit",
"&:hover": {
textDecoration: "underline",
},
}}
>
{t("pageSpeedLearnMoreLink")}
</Link>{" "}
{t("pageSpeedAddApiKey")}
</>
);
};
return (
<Box
position="relative"
@@ -78,138 +32,44 @@ const Fallback = ({
alignItems: "center",
}}
>
<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: "55vh",
md: "50vh",
lg: "70vh",
xl: "58vh",
},
width: {
sm: "90%",
md: "70%",
lg: "42%",
},
minHeight: {
sm: theme.spacing(280),
},
padding: theme.spacing(10),
}}
>
<FallbackContainer type={type}>
<FallbackBackground />
<Stack
className={`fallback__${title?.trim().split(" ")[0]}`}
gap={theme.spacing(5)}
zIndex={1}
alignItems="center"
gap={theme.spacing(20)}
>
{mode === "light" ? (
<Skeleton style={{ zIndex: 1 }} />
) : (
<SkeletonDark style={{ zIndex: 1 }} />
)}
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<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}
>
{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 && (
<Stack gap={theme.spacing(10)}>
<Button
variant="contained"
color="accent"
sx={{ alignSelf: "center", mb: "2px" }}
onClick={() => navigate(link)}
>
Let's create your first {title}
</Button>
{/* Bulk create of uptime monitors */}
{title === "uptime monitor" && (
<Button
variant="contained"
color="accent"
sx={{ alignSelf: "center" }}
onClick={() => navigate("/uptime/bulk-import")}
>
{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>
)}
</Stack>
)}
<FallbackTitle title={title} />
<FallbackCheckList
checks={checks}
title={title}
type={type}
/>
</Stack>
</Box>
{isAdmin && (
<Stack
gap={theme.spacing(10)}
alignItems="center"
>
<FallbackActionButtons
title={title}
link={link}
type={type}
/>
{children}
</Stack>
)}
</FallbackContainer>
</Box>
);
};
Fallback.propTypes = {
title: PropTypes.string.isRequired,
checks: PropTypes.arrayOf(PropTypes.string).isRequired,
link: PropTypes.string,
isAdmin: PropTypes.bool,
vowelStart: PropTypes.bool,
showPageSpeedWarning: PropTypes.bool,
type: PropTypes.string.isRequired,
children: PropTypes.node,
};
export default Fallback;
@@ -1,7 +1,7 @@
import { useTheme } from "@emotion/react";
import { Box, Stack } from "@mui/material";
import Skeleton from "../../assets/Images/create-placeholder.svg?react";
import SkeletonDark from "../../assets/Images/create-placeholder-dark.svg?react";
import OutputAnimation from "../../assets/Animations/output.gif";
import DarkmodeOutput from "../../assets/Animations/darkmodeOutput.gif";
import Background from "../../assets/Images/background-grid.svg?react";
import { useSelector } from "react-redux";
@@ -37,11 +37,18 @@ const GenericFallback = ({ children }) => {
marginTop: "100px",
}}
>
{mode === "light" ? (
<Skeleton style={{ zIndex: 1 }} />
) : (
<SkeletonDark style={{ zIndex: 1 }} />
)}
<Box
component="img"
src={mode === "light" ? OutputAnimation : DarkmodeOutput}
Background="transparent"
alt="Loading animation"
sx={{
zIndex: 1,
border: "none",
borderRadius: theme.spacing(8),
width: "100%",
}}
/>
<Box
sx={{
"& svg g g:last-of-type path": {
@@ -0,0 +1,51 @@
import { Stack, Typography } from "@mui/material";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
const DEFAULT_GAP = 6;
const FieldWrapper = ({
label,
children,
gap,
labelMb,
labelFontWeight = 500,
labelVariant = "h3",
labelSx = {},
sx = {},
}) => {
const theme = useTheme();
return (
<Stack
gap={gap ?? theme.spacing(DEFAULT_GAP)}
sx={sx}
>
{label && (
<Typography
component={labelVariant}
color={theme.palette.primary.contrastTextSecondary}
fontWeight={labelFontWeight}
sx={{
...(labelMb !== undefined && { mb: theme.spacing(labelMb) }),
...labelSx,
}}
>
{label}
</Typography>
)}
{children}
</Stack>
);
};
FieldWrapper.propTypes = {
label: PropTypes.node,
children: PropTypes.node.isRequired,
gap: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]),
labelMb: PropTypes.number,
labelFontWeight: PropTypes.number,
labelVariant: PropTypes.string,
labelSx: PropTypes.object,
sx: PropTypes.object,
};
export default FieldWrapper;
+19 -2
View File
@@ -24,7 +24,17 @@ import "./index.css";
* @returns {JSX.Element} - The rendered Radio component.
*/
const Radio = ({ name, checked, value, id, size, title, desc, onChange }) => {
const Radio = ({
name,
checked,
value,
id,
size,
title,
desc,
onChange,
labelSpacing,
}) => {
const theme = useTheme();
return (
@@ -53,7 +63,14 @@ const Radio = ({ name, checked, value, id, size, title, desc, onChange }) => {
onChange={onChange}
label={
<>
<Typography component="p">{title}</Typography>
<Typography
component="p"
mb={
labelSpacing !== undefined ? theme.spacing(labelSpacing) : theme.spacing(2)
}
>
{title}
</Typography>
<Typography
component="h6"
mt={theme.spacing(1)}
+34 -16
View File
@@ -4,7 +4,6 @@ import {
ListItem,
Autocomplete,
TextField,
Stack,
Typography,
Checkbox,
} from "@mui/material";
@@ -12,6 +11,7 @@ import { useTheme } from "@emotion/react";
import SearchIcon from "../../../assets/icons/search.svg?react";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import FieldWrapper from "../FieldWrapper";
/**
* Search component using Material UI's Autocomplete.
@@ -24,6 +24,7 @@ import { useTranslation } from "react-i18next";
* @param {Function} props.handleChange - Function to call when the input changes
* @param {Function} Prop.onBlur - Function to call when the input is blured
* @param {Object} props.sx - Additional styles to apply to the component
* @param {string} props.unit - Label to identify type of options
* @returns {JSX.Element} The rendered Search component
*/
@@ -31,7 +32,6 @@ const SearchAdornment = () => {
const theme = useTheme();
return (
<Box
mr={theme.spacing(4)}
height={16}
sx={{
"& svg": {
@@ -49,7 +49,7 @@ const SearchAdornment = () => {
);
};
//TODO keep search state inside of component
//TODO keep search state inside of component.
const Search = ({
label,
id,
@@ -68,6 +68,14 @@ const Search = ({
startAdornment,
endAdornment,
onBlur,
//FieldWrapper's props
gap,
labelMb,
labelFontWeight,
labelVariant,
labelSx = {},
unit = "option",
maxWidth = "100%",
}) => {
const theme = useTheme();
const { t } = useTranslation();
@@ -139,15 +147,17 @@ const Search = ({
getOptionLabel={(option) => option[filteredBy]}
isOptionEqualToValue={(option, value) => option._id === value._id} // Compare by unique identifier
renderInput={(params) => (
<Stack>
<Typography
component="h3"
fontSize={"var(--env-var-font-size-medium)"}
color={theme.palette.primary.contrastTextSecondary}
fontWeight={500}
>
{label}
</Typography>
<FieldWrapper
label={label}
labelMb={labelMb}
labelVariant={labelVariant}
labelFontWeight={labelFontWeight}
labelSx={labelSx}
gap={gap}
sx={{
...sx,
}}
>
<TextField
{...params}
error={Boolean(error)}
@@ -175,7 +185,7 @@ const Search = ({
{error}
</Typography>
)}
</Stack>
</FieldWrapper>
)}
filterOptions={(options, { inputValue }) => {
if (inputValue.trim() === "" && multiple && isAdorned) {
@@ -186,7 +196,12 @@ const Search = ({
);
if (filtered.length === 0) {
return [{ [filteredBy]: "No monitors found", noOptions: true }];
return [
{
[filteredBy]: t("general.noOptionsFound", { unit: unit }),
noOptions: true,
},
];
}
return filtered;
}}
@@ -267,7 +282,9 @@ const Search = ({
}}
sx={{
/* height: 34,*/
"&.MuiAutocomplete-root .MuiAutocomplete-input": { p: 0 },
"&.MuiAutocomplete-root .MuiAutocomplete-input": {
padding: `0 ${theme.spacing(5)}`,
},
...sx,
}}
/>
@@ -281,7 +298,7 @@ Search.propTypes = {
options: PropTypes.array.isRequired,
filteredBy: PropTypes.string.isRequired,
secondaryLabel: PropTypes.string,
value: PropTypes.array,
value: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
inputValue: PropTypes.string.isRequired,
handleInputChange: PropTypes.func.isRequired,
handleChange: PropTypes.func,
@@ -292,6 +309,7 @@ Search.propTypes = {
startAdornment: PropTypes.object,
endAdornment: PropTypes.object,
onBlur: PropTypes.func,
unit: PropTypes.string,
};
export default Search;
+29 -15
View File
@@ -2,6 +2,7 @@ import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { MenuItem, Select as MuiSelect, Stack, Typography } from "@mui/material";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import FieldWrapper from "../FieldWrapper";
import "./index.css";
@@ -49,9 +50,16 @@ const Select = ({
onChange,
onBlur,
sx,
error = false,
name = "",
labelControlSpacing = 2,
labelControlSpacing = 6,
maxWidth,
//FieldWrapper's props
labelMb,
labelFontWeight,
labelVariant,
labelSx = {},
fieldWrapperSx = {},
}) => {
const theme = useTheme();
const itemStyles = {
@@ -69,26 +77,24 @@ const Select = ({
};
return (
<Stack
gap={theme.spacing(labelControlSpacing)}
className="select-wrapper"
<FieldWrapper
label={label}
labelMb={labelMb}
labelVariant={labelVariant}
labelFontWeight={labelFontWeight}
labelSx={labelSx}
gap={labelControlSpacing}
sx={{
...fieldWrapperSx,
}}
>
{label && (
<Typography
component="h3"
color={theme.palette.primary.contrastTextSecondary}
fontWeight={500}
fontSize={13}
>
{label}
</Typography>
)}
<MuiSelect
className="select-component"
value={value}
onChange={onChange}
onBlur={onBlur}
displayEmpty
error={error}
name={name}
inputProps={{ id: id }}
IconComponent={KeyboardArrowDownIcon}
@@ -107,6 +113,13 @@ const Select = ({
"& svg path": {
fill: theme.palette.primary.contrastTextTertiary,
},
"& .MuiSelect-select": {
padding: "0",
minHeight: "34px",
display: "flex",
alignItems: "center",
lineHeight: 1,
},
...sx,
}}
renderValue={(selected) => {
@@ -151,7 +164,7 @@ const Select = ({
</MenuItem>
))}
</MuiSelect>
</Stack>
</FieldWrapper>
);
};
@@ -161,6 +174,7 @@ Select.propTypes = {
label: PropTypes.string,
placeholder: PropTypes.string,
isHidden: PropTypes.bool,
error: PropTypes.bool,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool])
.isRequired,
items: PropTypes.arrayOf(
@@ -2,6 +2,7 @@ import { Stack, TextField, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { forwardRef, useState, cloneElement } from "react";
import PropTypes from "prop-types";
import FieldWrapper from "../FieldWrapper";
const getSx = (theme, type, maxWidth) => {
const sx = {
@@ -85,31 +86,43 @@ const TextInput = forwardRef(
marginLeft,
disabled = false,
hidden = false,
//FieldWrapper's props
gap,
labelMb,
labelFontWeight,
labelVariant,
labelSx = {},
sx = {},
},
ref
) => {
const [fieldType, setFieldType] = useState(type);
const theme = useTheme();
const labelContent = label && (
<>
{label}
{isRequired && <Required />}
{isOptional && <Optional optionalLabel={optionalLabel} />}
</>
);
return (
<Stack
flex={flex}
display={hidden ? "none" : ""}
marginTop={marginTop}
marginRight={marginRight}
marginBottom={marginBottom}
marginLeft={marginLeft}
<FieldWrapper
label={labelContent}
labelMb={labelMb}
labelVariant={labelVariant}
labelFontWeight={labelFontWeight}
labelSx={labelSx}
gap={gap}
sx={{
flex,
display: hidden ? "none" : "",
mt: marginTop,
mr: marginRight,
mb: marginBottom,
ml: marginLeft,
...sx,
}}
>
<Typography
component="h3"
fontSize={"var(--env-var-font-size-medium)"}
color={theme.palette.primary.contrastTextSecondary}
fontWeight={500}
mb={theme.spacing(2)}
>
{label}
{isRequired && <Required />}
{isOptional && <Optional optionalLabel={optionalLabel} />}
</Typography>
<TextField
id={id}
name={name}
@@ -132,7 +145,7 @@ const TextInput = forwardRef(
}}
disabled={disabled}
/>
</Stack>
</FieldWrapper>
);
}
);
+26 -5
View File
@@ -6,6 +6,13 @@ import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import { setLanguage } from "../Features/UI/uiSlice";
const langMap = {
cs: "cz",
ja: "jp",
uk: "ua",
vi: "vn",
};
const LanguageSelector = () => {
const { i18n } = useTranslation();
const theme = useTheme();
@@ -23,30 +30,44 @@ const LanguageSelector = () => {
value={language}
onChange={handleChange}
size="small"
sx={{ minWidth: 80 }}
sx={{
minWidth: 80,
"& .MuiSelect-select": {
display: "flex",
alignItems: "center",
justifyContent: "center",
},
"& .MuiSelect-icon": {
alignSelf: "center",
},
}}
>
{languages.map((lang) => {
let parsedLang = lang === "en" ? "gb" : lang;
// Fix for Czech
if (parsedLang === "cs") {
parsedLang = "cz";
}
if (parsedLang.includes("-")) {
parsedLang = parsedLang.split("-")[1].toLowerCase();
}
parsedLang = langMap[parsedLang] || parsedLang;
const flag = parsedLang ? `fi fi-${parsedLang}` : null;
return (
<MenuItem
key={lang}
value={lang}
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<Stack
direction="row"
spacing={theme.spacing(2)}
alignItems="center"
justifyContent="center"
>
<Box
component="span"
@@ -12,13 +12,13 @@
}
} */
.home-layout aside {
/* .home-layout aside {
position: sticky;
top: 0;
left: 0;
height: 100vh;
max-width: var(--env-var-side-bar-width);
}
} */
.home-layout > div {
min-height: calc(100vh - var(--env-var-spacing-2) * 2);
@@ -10,7 +10,18 @@ import { useState, useEffect } from "react";
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
const NotificationConfig = ({ notifications, setMonitor, setNotifications }) => {
const NotificationConfig = ({
notifications,
setMonitor,
setNotifications,
//FieldWrapper's props
gap,
labelMb,
labelFontWeight,
labelVariant,
labelSx = {},
sx = {},
}) => {
// Local state
const [notificationsSearch, setNotificationsSearch] = useState("");
const [selectedNotifications, setSelectedNotifications] = useState([]);
@@ -66,6 +77,14 @@ const NotificationConfig = ({ notifications, setMonitor, setNotifications }) =>
handleChange={(value) => {
handleSearch(value);
}}
labelMb={labelMb}
labelVariant={labelVariant}
labelFontWeight={labelFontWeight}
labelSx={labelSx}
gap={gap}
sx={{
...sx,
}}
/>
<Stack
flex={1}
@@ -0,0 +1,66 @@
import GenericFallback from "../GenericFallback";
import Fallback from "../Fallback";
import { Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useIsAdmin } from "../../Hooks/useIsAdmin";
import PropTypes from "prop-types";
const PageStateWrapper = ({
networkError,
isLoading,
items,
type,
fallbackLink,
fallbackChildren,
children,
}) => {
const theme = useTheme();
const { t } = useTranslation();
const isAdmin = useIsAdmin();
const isEmpty = items === null || (Array.isArray(items) && items.length === 0);
const hasInitialized = items !== undefined && !(isLoading && isEmpty);
const showEmpty = !isLoading && isEmpty;
if (networkError) {
return (
<GenericFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{t("common.toasts.networkError")}
</Typography>
<Typography>{t("common.toasts.checkConnection")}</Typography>
</GenericFallback>
);
}
if (!hasInitialized) {
return null;
}
if (showEmpty) {
return (
<Fallback
type={type}
title={t(`${type}.fallback.title`)}
checks={t(`${type}.fallback.checks`, { returnObjects: true })}
link={fallbackLink}
isAdmin={isAdmin}
>
{fallbackChildren}
</Fallback>
);
}
return <>{children}</>;
};
PageStateWrapper.propTypes = {
networkError: PropTypes.any,
isLoading: PropTypes.bool,
items: PropTypes.oneOfType([PropTypes.array, PropTypes.oneOf([null])]),
type: PropTypes.string,
fallbackLink: PropTypes.string,
fallbackChildren: PropTypes.node,
children: PropTypes.node,
};
export default PageStateWrapper;
@@ -0,0 +1,277 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Tooltip from "@mui/material/Tooltip";
import IconButton from "@mui/material/IconButton";
import Avatar from "../../Avatar";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import Divider from "@mui/material/Divider";
import DotsVertical from "../../../assets/icons/dots-vertical.svg?react";
import LogoutSvg from "../../../assets/icons/logout.svg?react";
import { useTheme } from "@emotion/react";
import { useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { useState } from "react";
import { useNavigate } from "react-router";
import { clearAuthState } from "../../../Features/Auth/authSlice";
import { useDispatch } from "react-redux";
import PropTypes from "prop-types";
const getFilteredAccountMenuItems = (user, items) => {
if (!user) return [];
let filtered = [...items];
if (user.role?.includes("demo")) {
filtered = filtered.filter((item) => item.name !== "Password");
}
if (!user.role?.includes("superadmin")) {
filtered = filtered.filter((item) => item.name !== "Team");
}
return filtered;
};
const getRoleDisplayText = (user, t) => {
if (!user?.role) return "";
if (user.role.includes("superadmin")) return t("roles.superAdmin");
if (user.role.includes("admin")) return t("roles.admin");
if (user.role.includes("user")) return t("roles.teamMember");
if (user.role.includes("demo")) return t("roles.demoUser");
return user.role;
};
const AuthFooter = ({ collapsed, accountMenuItems }) => {
const { t } = useTranslation();
const theme = useTheme();
const authState = useSelector((state) => state.auth);
const navigate = useNavigate();
const dispatch = useDispatch();
const [anchorEl, setAnchorEl] = useState(null);
const openPopup = (event) => {
setAnchorEl(event.currentTarget);
};
const closePopup = () => {
setAnchorEl(null);
};
const logout = async () => {
dispatch(clearAuthState());
navigate("/login");
};
const renderAccountMenuItems = (user, items) => {
const filteredItems = getFilteredAccountMenuItems(user, items);
return filteredItems.map((item) => (
<MenuItem
key={item.name}
onClick={() => {
closePopup();
navigate(item.path);
}}
sx={{
gap: theme.spacing(2),
borderRadius: theme.shape.borderRadius,
pl: theme.spacing(4),
}}
>
{item.icon}
{item.name}
</MenuItem>
));
};
return (
<Stack
direction="row"
height="var(--env-var-side-bar-auth-footer-height)"
alignItems="center"
py={theme.spacing(4)}
px={theme.spacing(8)}
gap={theme.spacing(2)}
borderRadius={theme.shape.borderRadius}
boxSizing={"border-box"}
>
<Avatar
small={true}
onClick={(e) => collapsed && openPopup(e)}
sx={{
cursor: collapsed ? "pointer" : "default",
}}
/>
<Stack
direction={"row"}
alignItems={"center"}
gap={theme.spacing(2)}
minWidth={0}
maxWidth={collapsed ? 0 : "100%"}
sx={{
opacity: collapsed ? 0 : 1,
transition: "opacity 300ms ease, max-width 300ms ease",
transitionDelay: collapsed ? "0ms" : "300ms",
}}
>
<Stack
ml={theme.spacing(2)}
sx={{
maxWidth: "50%",
overflow: "hidden",
}}
>
<Typography
color={theme.palette.primary.contrastText}
fontWeight={500}
lineHeight={1}
fontSize={"var(--env-var-font-size-medium)"}
sx={{
display: "block",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{authState.user?.firstName} {authState.user?.lastName}
</Typography>
<Typography
color={theme.palette.primary.contrastText}
fontSize={"var(--env-var-font-size-small)"}
textOverflow="ellipsis"
overflow="hidden"
whiteSpace="nowrap"
sx={{ textTransform: "capitalize", opacity: 0.8 }}
>
{getRoleDisplayText(authState.user, t)}
</Typography>
</Stack>
<Tooltip
title={t("navControls")}
disableInteractive
>
<IconButton
sx={{
ml: "50px",
"&:focus": { outline: "none" },
alignSelf: "center",
"& svg": {
width: "22px",
height: "22px",
},
"& svg path": {
/* Vertical three dots */
stroke: theme.palette.primary.contrastTextTertiary,
},
}}
onClick={(event) => openPopup(event)}
>
<DotsVertical />
</IconButton>
</Tooltip>
</Stack>
<Menu
className="sidebar-popup"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={closePopup}
disableScrollLock
anchorOrigin={{
vertical: "top",
horizontal: "right",
}}
slotProps={{
paper: {
sx: {
marginTop: theme.spacing(-4),
marginLeft: collapsed ? theme.spacing(2) : 0,
},
},
}}
MenuListProps={{
sx: {
p: 2,
"& li": { m: 0 },
"& li:has(.MuiBox-root):hover": {
backgroundColor: "transparent",
},
},
}}
sx={{
ml: theme.spacing(4),
}}
>
{collapsed && (
<MenuItem sx={{ cursor: "default", minWidth: "50%" }}>
<Box
mb={theme.spacing(2)}
sx={{
minWidth: "50%",
maxWidth: "max-content",
overflow: "visible",
whiteSpace: "nowrap",
}}
>
<Typography
component="span"
fontWeight={500}
fontSize={13}
sx={{
display: "block",
whiteSpace: "nowrap",
overflow: "visible",
// wordBreak: "break-word",
textOverflow: "clip",
}}
>
{authState.user?.firstName} {authState.user?.lastName}
</Typography>
<Typography
sx={{
textTransform: "capitalize",
fontSize: 12,
whiteSpace: "nowrap",
overflow: "visible",
// wordBreak: "break-word",
}}
>
{authState.user?.role}
</Typography>
</Box>
</MenuItem>
)}
{/* TODO Do we need two dividers? */}
{collapsed && <Divider />}
{/* <Divider /> */}
{renderAccountMenuItems(authState.user, accountMenuItems)}
<MenuItem
onClick={logout}
sx={{
gap: theme.spacing(4),
borderRadius: theme.shape.borderRadius,
pl: theme.spacing(4),
"& svg path": {
stroke: theme.palette.primary.contrastTextTertiary,
},
}}
>
<LogoutSvg />
{t("menu.logOut", "Log out")}
</MenuItem>
</Menu>
</Stack>
);
};
AuthFooter.propTypes = {
collapsed: PropTypes.bool,
accountMenuItems: PropTypes.array,
};
export default AuthFooter;
@@ -0,0 +1,55 @@
import IconButton from "@mui/material/IconButton";
import ArrowRight from "../../ArrowRight";
import ArrowLeft from "../../ArrowLeft";
import { useTheme } from "@mui/material/styles";
import { useDispatch } from "react-redux";
import { toggleSidebar } from "../../../Features/UI/uiSlice";
import PropTypes from "prop-types";
const CollapseButton = ({ collapsed }) => {
const theme = useTheme();
const dispatch = useDispatch();
const arrowIcon = collapsed ? (
<ArrowRight
height={theme.spacing(8)}
width={theme.spacing(8)}
color={theme.palette.primary.contrastTextSecondary}
/>
) : (
<ArrowLeft
height={theme.spacing(8)}
width={theme.spacing(8)}
color={theme.palette.primary.contrastTextSecondary}
/>
);
return (
<IconButton
sx={{
position: "absolute",
/* TODO 60 is a magic number. if logo chnges size this might break */
top: 60,
right: 0,
transform: `translate(50%, 0)`,
backgroundColor: theme.palette.tertiary.main,
border: `1px solid ${theme.palette.primary.lowContrast}`,
p: theme.spacing(2.5),
"&:focus": { outline: "none" },
"&:hover": {
backgroundColor: theme.palette.primary.lowContrast,
borderColor: theme.palette.primary.lowContrast,
},
}}
onClick={() => {
dispatch(toggleSidebar());
}}
>
{arrowIcon}
</IconButton>
);
};
CollapseButton.propTypes = {
collapsed: PropTypes.bool.isRequired,
};
export default CollapseButton;
@@ -0,0 +1,67 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
const Logo = ({ collapsed }) => {
const { t } = useTranslation();
const theme = useTheme();
const navigate = useNavigate();
return (
<Stack
pt={theme.spacing(6)}
pb={theme.spacing(12)}
pl={theme.spacing(8)}
direction="row"
alignItems="center"
gap={theme.spacing(4)}
onClick={() => navigate("/")}
sx={{ cursor: "pointer" }}
>
<Typography
pl={theme.spacing("1px")}
minWidth={theme.spacing(16)}
minHeight={theme.spacing(16)}
display={"flex"}
justifyContent={"center"}
alignItems={"center"}
backgroundColor={theme.palette.accent.main}
borderRadius={theme.shape.borderRadius}
color={theme.palette.accent.contrastText}
fontSize={18}
>
C
</Typography>
<Box
overflow={"hidden"}
sx={{
transition: "opacity 900ms ease, width 900ms ease",
opacity: collapsed ? 0 : 1,
whiteSpace: "nowrap",
width: collapsed ? 0 : "100%",
}}
>
{" "}
<Typography
lineHeight={1}
mt={theme.spacing(2)}
color={theme.palette.primary.contrastText}
fontSize={"var(--env-var-font-size-medium-plus)"}
sx={{ fontWeight: 500 }}
>
{t("common.appName")}
</Typography>
</Box>
</Stack>
);
};
Logo.propTypes = {
collapsed: PropTypes.bool,
};
export default Logo;
@@ -0,0 +1,97 @@
import Tooltip from "@mui/material/Tooltip";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
const NavItem = ({ item, collapsed, selected, onClick }) => {
const theme = useTheme();
const iconStroke = selected
? theme.palette.primary.contrastText
: theme.palette.primary.contrastTextTertiary;
const buttonBgColor = selected ? theme.palette.secondary.main : "transparent";
const buttonBgHoverColor = selected
? theme.palette.secondary.main
: theme.palette.tertiary.main;
const fontWeight = selected ? 600 : 400;
return (
<Tooltip
placement="right"
title={collapsed ? item.name : ""}
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: {
offset: [0, -16],
},
},
],
},
}}
disableInteractive
>
<ListItemButton
sx={{
backgroundColor: buttonBgColor,
"&:hover": {
backgroundColor: buttonBgHoverColor,
},
height: 37,
gap: theme.spacing(4),
borderRadius: theme.shape.borderRadius,
px: theme.spacing(4),
pl: theme.spacing(5),
}}
onClick={onClick}
>
<ListItemIcon
sx={{
minWidth: 0,
"& svg": {
height: 20,
width: 20,
opacity: 0.81,
},
"& svg path": {
stroke: iconStroke,
},
}}
>
{item.icon}
</ListItemIcon>
<Box
sx={{
overflow: "hidden",
transition: "opacity 900ms ease",
opacity: collapsed ? 0 : 1,
whiteSpace: "nowrap",
}}
>
<Typography
variant="body1"
color={theme.palette.primary.contrastText}
sx={{
fontWeight: fontWeight,
opacity: 0.9,
}}
>
{item.name}
</Typography>
</Box>
</ListItemButton>
</Tooltip>
);
};
NavItem.propTypes = {
item: PropTypes.object,
collapsed: PropTypes.bool,
selected: PropTypes.bool,
onClick: PropTypes.func,
};
export default NavItem;
-106
View File
@@ -1,106 +0,0 @@
/* TODO */
aside .MuiList-root svg {
width: 20px;
height: 20px;
opacity: 0.9;
}
aside span.MuiTypography-root {
font-size: var(--env-var-font-size-medium);
line-height: 1;
}
aside .MuiStack-root + span.MuiTypography-root {
font-size: var(--env-var-font-size-medium-plus);
}
aside .MuiListSubheader-root {
font-size: var(--env-var-font-size-small);
font-weight: 500;
line-height: 1.5;
text-transform: uppercase;
margin-bottom: 2px;
opacity: 0.6;
}
aside p.MuiTypography-root {
font-size: var(--env-var-font-size-small);
opacity: 0.8;
}
aside .MuiListItemButton-root:not(.selected-path) > * {
opacity: 0.9;
}
aside .selected-path > * {
opacity: 1;
}
aside .selected-path span.MuiTypography-root {
font-weight: 600;
}
aside .MuiCollapse-wrapperInner .MuiList-root > .MuiListItemButton-root {
position: relative;
}
aside .MuiCollapse-wrapperInner .MuiList-root svg,
aside .MuiList-root .MuiListItemText-root + svg {
width: 18px;
height: 18px;
}
.sidebar-popup li.MuiButtonBase-root:has(.MuiBox-root) {
padding-bottom: 0;
}
.sidebar-popup svg {
width: 16px;
height: 16px;
opacity: 0.9;
}
/* TRANSITIONS */
aside {
flex: 1;
transition: max-width 650ms cubic-bezier(0.36, -0.01, 0, 0.77);
}
.home-layout aside.collapsed {
max-width: 64px;
}
aside.expanded .MuiTypography-root,
aside.expanded p.MuiTypography-root,
aside.expanded .MuiListItemText-root + svg,
aside.expanded .MuiAvatar-root + .MuiBox-root + .MuiIconButton-root {
visibility: visible;
animation: fadeIn 1s ease;
}
aside.collapsed .MuiTypography-root,
aside.collapsed p.MuiTypography-root,
aside.collapsed .MuiListItemText-root + svg,
aside.collapsed .MuiAvatar-root + .MuiBox-root + .MuiIconButton-root {
opacity: 0;
visibility: hidden;
}
aside .MuiListSubheader-root {
transition: padding 200ms ease;
}
.sidebar-delay-fade {
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease;
}
aside.expanded.sidebar-ready .sidebar-delay-fade {
opacity: 1;
visibility: visible;
}
@keyframes fadeIn {
0% {
opacity: 0;
visibility: hidden;
}
30% {
opacity: 0;
visibility: hidden;
}
100% {
opacity: 0.9;
visibility: visible;
}
}
+78 -780
View File
@@ -1,26 +1,16 @@
import React, { useEffect, useState, useRef } from "react";
import {
Box,
Collapse,
Divider,
IconButton,
List,
ListItemButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Stack,
Tooltip,
Typography,
} from "@mui/material";
import ThemeSwitch from "../ThemeSwitch";
import Avatar from "../Avatar";
import Stack from "@mui/material/Stack";
import List from "@mui/material/List";
import Logo from "./components/logo";
import CollapseButton from "./components/collapseButton";
import Divider from "@mui/material/Divider";
import NavItem from "./components/navItem";
import AuthFooter from "./components/authFooter";
import StarPrompt from "../StarPrompt";
import LockSvg from "../../assets/icons/lock.svg?react";
import UserSvg from "../../assets/icons/user.svg?react";
import TeamSvg from "../../assets/icons/user-two.svg?react";
import LogoutSvg from "../../assets/icons/logout.svg?react";
import Support from "../../assets/icons/support.svg?react";
import Maintenance from "../../assets/icons/maintenance.svg?react";
import Monitors from "../../assets/icons/monitors.svg?react";
@@ -28,11 +18,6 @@ import Incidents from "../../assets/icons/incidents.svg?react";
import Integrations from "../../assets/icons/integrations.svg?react";
import PageSpeed from "../../assets/icons/page-speed.svg?react";
import Settings from "../../assets/icons/settings.svg?react";
import ArrowDown from "../../assets/icons/down-arrow.svg?react";
import ArrowUp from "../../assets/icons/up-arrow.svg?react";
import ArrowRight from "../../assets/icons/right-arrow.svg?react";
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 StatusPages from "../../assets/icons/status-pages.svg?react";
@@ -40,17 +25,18 @@ import Discussions from "../../assets/icons/discussions.svg?react";
import Notifications from "../../assets/icons/notifications.svg?react";
import Logs from "../../assets/icons/logs.svg?react";
import "./index.css";
// Utils
import { useLocation, useNavigate } from "react-router";
import { useTheme } from "@emotion/react";
import { useDispatch, useSelector } from "react-redux";
import { useTheme } from "@mui/material/styles";
import { useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { clearAuthState } from "../../Features/Auth/authSlice";
import { toggleSidebar } from "../../Features/UI/uiSlice";
import { TurnedIn } from "@mui/icons-material";
import { rules } from "eslint-plugin-react-refresh";
import { useNavigate } from "react-router";
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/checkmate/releases",
};
const getMenu = (t) => [
{ name: t("menu.uptime"), path: "uptime", icon: <Monitors /> },
@@ -92,782 +78,94 @@ const getAccountMenuItems = (t) => [
{ name: t("menu.team"), path: "account/team", icon: <TeamSvg /> },
];
/* TODO this could be a key in nested Path would be the link */
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/checkmate/releases",
};
const PATH_MAP = {
monitors: "Dashboard",
pagespeed: "Dashboard",
infrastructure: "Dashboard",
account: "Account",
settings: "Settings",
};
/**
* @component
* Sidebar component serves as a sidebar containing a menu.
*
* @returns {JSX.Element} The JSX element representing the Sidebar component.
*/
function Sidebar() {
const Sidebar = () => {
const theme = useTheme();
const navigate = useNavigate();
const location = useLocation();
const dispatch = useDispatch();
const { t } = useTranslation();
const authState = useSelector((state) => state.auth);
const navigate = useNavigate();
// Redux state
const collapsed = useSelector((state) => state.ui.sidebar.collapsed);
const menu = getMenu(t);
const otherMenuItems = getOtherMenuItems(t);
const accountMenuItems = getAccountMenuItems(t);
const collapsed = useSelector((state) => state.ui.sidebar.collapsed);
const [open, setOpen] = useState({ Dashboard: false, Account: false, Other: false });
const [anchorEl, setAnchorEl] = useState(null);
const [popup, setPopup] = useState();
const { user } = useSelector((state) => state.auth);
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) {
setSidebarReady(false);
const timeout = setTimeout(() => {
setSidebarReady(true);
}, TRANSITION_DURATION);
return () => clearTimeout(timeout);
} else {
setSidebarReady(false);
}
}, [collapsed]);
const renderAccountMenuItems = () => {
let filteredAccountMenuItems = [...accountMenuItems];
// If the user is in demo mode, remove the "Password" option
if (user.role?.includes("demo")) {
filteredAccountMenuItems = filteredAccountMenuItems.filter(
(item) => item.name !== "Password"
);
}
// If the user is NOT a superadmin, remove the "Team" option
if (user.role && !user.role.includes("superadmin")) {
filteredAccountMenuItems = filteredAccountMenuItems.filter(
(item) => item.name !== "Team"
);
}
return filteredAccountMenuItems.map((item) => (
<MenuItem
key={item.name}
onClick={() => {
closePopup();
navigate(item.path);
}}
sx={{
gap: theme.spacing(2),
borderRadius: theme.shape.borderRadius,
pl: theme.spacing(4),
}}
>
{item.icon}
{item.name}
</MenuItem>
));
};
const openPopup = (event, id) => {
setAnchorEl(event.currentTarget);
setPopup(id);
};
const closePopup = () => {
setAnchorEl(null);
};
/**
* Handles logging out the user
*
*/
const logout = async () => {
// Clear auth state
dispatch(clearAuthState());
navigate("/login");
};
useEffect(() => {
const matchedKey = Object.keys(PATH_MAP).find((key) =>
location.pathname.includes(key)
);
if (matchedKey) {
setOpen((prev) => ({ ...prev, [PATH_MAP[matchedKey]]: true }));
}
}, [location]);
const iconColor = theme.palette.primary.contrastTextTertiary;
const sidebarClassName = `${collapsed ? "collapsed" : "expanded"} ${sidebarReady ? "sidebar-ready" : ""}`;
/* TODO refactor this, there are a some ternaries and comments in the return */
return (
<Stack
height="100vh"
width={
collapsed
? "var(--env-var-side-bar-collapsed-width)"
: "var(--env-var-side-bar-width)"
}
component="aside"
ref={sidebarRef}
className={sidebarClassName}
/* TODO general padding should be here */
py={theme.spacing(6)}
position="sticky"
top={0}
borderRight={`1px solid ${theme.palette.primary.lowContrast}`}
paddingTop={theme.spacing(6)}
paddingBottom={theme.spacing(6)}
gap={theme.spacing(6)}
/* TODO set all style in this sx if possible (when general)
This is the top lever for styles
*/
sx={{
position: "relative",
borderRight: `1px solid ${theme.palette.primary.lowContrast}`,
borderColor: theme.palette.primary.lowContrast,
borderRadius: 0,
"& :is(p, span, .MuiListSubheader-root)": {
/*
Text color for unselected menu items and menu headings
Secondary contrast text against main background
*/
color: theme.palette.primary.contrastTextSecondary,
},
"& .MuiList-root svg path": {
/* Menu Icons */
stroke: iconColor,
},
"& .selected-path": {
/* Selected menu item */
backgroundColor: theme.palette.secondary.main,
"&:hover": {
backgroundColor: theme.palette.secondary.main,
},
"& .MuiListItemIcon-root svg path": {
/* Selected menu item icon */
stroke: theme.palette.secondary.contrastText,
},
"& .MuiListItemText-root :is(p, span)": {
/* Selected menu item text */
color: theme.palette.secondary.contrastText,
},
},
"& .MuiListItemButton-root:not(.selected-path)": {
transition: "background-color .3s",
" &:hover": {
/* Hovered menu item bg color */
backgroundColor: theme.palette.tertiary.main,
"& :is(p, span)": {
/* Hovered menu item text color */
color: theme.palette.tertiary.contrastText,
},
},
},
transition: "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)",
}}
>
<IconButton
<CollapseButton collapsed={collapsed} />
<Logo collapsed={collapsed} />
<List
component="nav"
aria-labelledby="nested-menu-subheader"
disablePadding
sx={{
position: "absolute",
/* TODO 60 is a magic number. if logo chnges size this might break */
top: 60,
right: 0,
transform: `translate(50%, 0)`,
backgroundColor: theme.palette.tertiary.main,
border: 1,
borderColor: theme.palette.primary.lowContrast,
p: theme.spacing(2.5),
"& svg": {
width: theme.spacing(8),
height: theme.spacing(8),
"& path": {
/* TODO this should be set at the top level if possible */
stroke: theme.palette.primary.contrastTextSecondary,
},
},
"&:focus": { outline: "none" },
"&:hover": {
backgroundColor: theme.palette.primary.lowContrast,
borderColor: theme.palette.primary.lowContrast,
},
px: theme.spacing(6),
height: "100%",
}}
onClick={() => {
setOpen((prev) =>
Object.fromEntries(Object.keys(prev).map((key) => [key, false]))
>
{menu.map((item) => {
const selected = location.pathname.startsWith(`/${item.path}`);
return (
<NavItem
key={item.path}
item={item}
collapsed={collapsed}
selected={selected}
onClick={() => navigate(`/${item.path}`)}
/>
);
dispatch(toggleSidebar());
}}
>
{collapsed ? <ArrowRight /> : <ArrowLeft />}
</IconButton>
{/* TODO Alignment done using padding. Use single source of truth to that*/}
<Stack
pt={theme.spacing(6)}
pb={theme.spacing(12)}
pl={theme.spacing(8)}
>
{/* TODO Abstract logo into component */}
{/* TODO Turn logo into a link */}
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(4)}
onClick={() => navigate("/")}
sx={{ cursor: "pointer" }}
>
<Stack
justifyContent="center"
alignItems="center"
minWidth={theme.spacing(16)}
minHeight={theme.spacing(16)}
pl="1px"
fontSize={18}
color={theme.palette.accent.contrastText}
sx={{
position: "relative",
backgroundColor: theme.palette.accent.main,
color: theme.palette.accent.contrastText,
borderRadius: theme.shape.borderRadius,
userSelect: "none",
}}
>
C
</Stack>
<Typography
component="span"
mt={theme.spacing(2)}
sx={{ opacity: 0.8, fontWeight: 500 }}
>
{t("common.appName")}
</Typography>
</Stack>
</Stack>
<Box
sx={{
flexGrow: 1,
overflow: "auto",
overflowX: "hidden",
"&::-webkit-scrollbar": {
width: theme.spacing(2),
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.primary.lowContrast,
borderRadius: theme.spacing(4),
},
}}
>
<List
component="nav"
aria-labelledby="nested-menu-subheader"
disablePadding
sx={{
px: theme.spacing(6),
height: "100%",
/* overflow: "hidden", */
}}
>
{menu.map((item) => {
return item.path ? (
/* If item has a path */
<Tooltip
key={item.path}
placement="right"
title={collapsed ? item.name : ""}
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: {
offset: [0, -16],
},
},
],
},
}}
disableInteractive
>
<ListItemButton
className={
location.pathname.startsWith(`/${item.path}`) ? "selected-path" : ""
}
onClick={() => navigate(`/${item.path}`)}
sx={{
height: "37px",
gap: theme.spacing(4),
borderRadius: theme.shape.borderRadius,
px: theme.spacing(4),
pl: theme.spacing(5),
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
<ListItemText>{item.name}</ListItemText>
</ListItemButton>
</Tooltip>
) : collapsed ? (
/* TODO Do we ever get here? If item does not have a path and collapsed state is true */
<React.Fragment key={item.name}>
<Tooltip
placement="right"
title={collapsed ? item.name : ""}
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: {
offset: [0, -16],
},
},
],
},
}}
disableInteractive
>
<ListItemButton
className={
Boolean(anchorEl) && popup === item.name ? "selected-path" : ""
}
onClick={(event) => openPopup(event, item.name)}
sx={{
position: "relative",
gap: theme.spacing(4),
borderRadius: theme.shape.borderRadius,
px: theme.spacing(4),
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
<ListItemText>{item.name}</ListItemText>
</ListItemButton>
</Tooltip>
<Menu
className="sidebar-popup"
anchorEl={anchorEl}
open={Boolean(anchorEl) && popup === item.name}
onClose={closePopup}
disableScrollLock
anchorOrigin={{
vertical: "top",
horizontal: "right",
}}
slotProps={{
paper: {
sx: {
mt: theme.spacing(-2),
ml: theme.spacing(1),
},
},
}}
MenuListProps={{ sx: { px: 1, py: 2 } }}
sx={{
ml: theme.spacing(8),
/* TODO what is this selection? */
"& .selected-path": {
backgroundColor: theme.palette.tertiary.main,
},
}}
>
{item.nested.map((child) => {
if (
child.name === "Team" &&
authState.user?.role &&
!authState.user.role.includes("superadmin")
) {
return null;
}
return (
<MenuItem
className={
location.pathname.includes(child.path) ? "selected-path" : ""
}
key={child.path}
onClick={() => {
const url = URL_MAP[child.path];
if (url) {
window.open(url, "_blank", "noreferrer");
} else {
navigate(`/${child.path}`);
}
closePopup();
}}
sx={{
gap: theme.spacing(4),
opacity: 0.9,
/* TODO this has no effect? */
"& svg": {
"& path": {
stroke: theme.palette.primary.contrastTextTertiary,
strokeWidth: 1.1,
},
},
}}
>
{child.icon}
{child.name}
</MenuItem>
);
})}
</Menu>
</React.Fragment>
) : (
/* TODO Do we ever get here? If item does not have a path and collapsed state is false */
<React.Fragment key={item.name}>
<ListItemButton
onClick={() =>
setOpen((prev) => ({
...Object.fromEntries(Object.keys(prev).map((key) => [key, false])),
[item.name]: !prev[item.name],
}))
}
sx={{
gap: theme.spacing(4),
borderRadius: theme.shape.borderRadius,
px: theme.spacing(4),
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
<ListItemText>{item.name}</ListItemText>
{open[`${item.name}`] ? <ArrowUp /> : <ArrowDown />}
</ListItemButton>
<Collapse
in={open[`${item.name}`]}
timeout="auto"
>
<List
component="div"
disablePadding
sx={{ pl: theme.spacing(12) }}
>
{item.nested.map((child) => {
if (
child.name === "Team" &&
authState.user?.role &&
!authState.user.role.includes("superadmin")
) {
return null;
}
return (
<ListItemButton
className={
location.pathname.includes(child.path) ? "selected-path" : ""
}
key={child.path}
onClick={() => {
const url = URL_MAP[child.path];
if (url) {
window.open(url, "_blank", "noreferrer");
} else {
navigate(`/${child.path}`);
}
}}
sx={{
gap: theme.spacing(4),
borderRadius: theme.shape.borderRadius,
pl: theme.spacing(4),
"&::before": {
content: `""`,
position: "absolute",
top: 0,
left: "-7px",
height: "100%",
borderLeft: 1,
borderLeftColor: theme.palette.primary.lowContrast,
},
"&:last-child::before": {
height: "50%",
},
"&::after": {
content: `""`,
position: "absolute",
top: "45%",
left: "-8px",
height: "3px",
width: "3px",
borderRadius: "50%",
backgroundColor: theme.palette.primary.lowContrast,
},
"&.selected-path::after": {
/* TODO what is this selector doing? */
backgroundColor: theme.palette.primary.contrastTextTertiary,
transform: "scale(1.2)",
},
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>{child.icon}</ListItemIcon>
<ListItemText>{child.name}</ListItemText>
</ListItemButton>
);
})}
</List>
</Collapse>
</React.Fragment>
);
})}
</List>
</Box>
})}
</List>
{!collapsed && <StarPrompt />}
<List
component="nav"
disablePadding
sx={{ px: theme.spacing(6) }}
>
{otherMenuItems.map((item) => {
return item.path ? (
<Tooltip
const selected = location.pathname.startsWith(`/${item.path}`);
return (
<NavItem
key={item.path}
placement="right"
title={collapsed ? item.name : ""}
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: {
offset: [0, -16],
},
},
],
},
}}
disableInteractive
>
<ListItemButton
className={
location.pathname.startsWith(`/${item.path}`) ? "selected-path" : ""
item={item}
collapsed={collapsed}
selected={selected}
onClick={() => {
const url = URL_MAP[item.path];
if (url) {
window.open(url, "_blank", "noreferrer");
} else {
navigate(`/${item.path}`);
}
onClick={() => {
const url = URL_MAP[item.path];
if (url) {
window.open(url, "_blank", "noreferrer");
} else {
navigate(`/${item.path}`);
}
}}
sx={{
height: "37px",
gap: theme.spacing(4),
borderRadius: theme.shape.borderRadius,
px: theme.spacing(4),
pl: theme.spacing(5),
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon} </ListItemIcon>
<ListItemText>{item.name} </ListItemText>{" "}
</ListItemButton>
</Tooltip>
) : null;
}}
/>
);
})}
</List>
<Divider sx={{ mt: "auto", borderColor: theme.palette.primary.lowContrast }} />
<Stack
direction="row"
height="50px"
alignItems="center"
py={theme.spacing(4)}
px={theme.spacing(8)}
gap={theme.spacing(2)}
borderRadius={theme.shape.borderRadius}
>
{collapsed ? (
<>
<Tooltip
title="Options"
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: {
offset: [0, -10],
},
},
],
},
}}
disableInteractive
>
<IconButton
onClick={(event) => openPopup(event, "logout")}
sx={{ p: 0, "&:focus": { outline: "none" } }}
>
<Avatar small={true} />
</IconButton>
</Tooltip>
</>
) : (
<>
<Avatar small={true} />
<Box
ml={theme.spacing(2)}
sx={{ maxWidth: "50%", overflow: "hidden" }}
>
<Typography
component="span"
fontWeight={500}
sx={{
display: "block",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{authState.user?.firstName} {authState.user?.lastName}
</Typography>
<Typography sx={{ textTransform: "capitalize" }}>
{authState.user?.role?.includes("superadmin")
? t("roles.superAdmin")
: authState.user?.role?.includes("admin")
? t("roles.admin")
: authState.user?.role?.includes("user")
? t("roles.teamMember")
: authState.user?.role?.includes("demo")
? t("roles.demoUser")
: authState.user?.role}
</Typography>
</Box>
<Stack
className="sidebar-delay-fade"
flexDirection={"row"}
marginLeft={"auto"}
columnGap={theme.spacing(2)}
>
<ThemeSwitch color={iconColor} />
<Tooltip
title={t("navControls")}
disableInteractive
>
<IconButton
sx={{
ml: "auto",
mr: "-8px",
"&:focus": { outline: "none" },
alignSelf: "center",
padding: "10px",
"& svg": {
width: "22px",
height: "22px",
},
"& svg path": {
/* Vertical three dots */
stroke: iconColor,
},
}}
onClick={(event) => openPopup(event, "logout")}
>
<DotsVertical />
</IconButton>
</Tooltip>
</Stack>
</>
)}
<Menu
className="sidebar-popup"
anchorEl={anchorEl}
open={Boolean(anchorEl) && popup === "logout"}
onClose={closePopup}
disableScrollLock
anchorOrigin={{
vertical: "top",
horizontal: "right",
}}
slotProps={{
paper: {
sx: {
marginTop: theme.spacing(-4),
marginLeft: collapsed ? theme.spacing(2) : 0,
},
},
}}
MenuListProps={{
sx: {
p: 2,
"& li": { m: 0 },
"& li:has(.MuiBox-root):hover": {
backgroundColor: "transparent",
},
},
}}
sx={{
ml: theme.spacing(4),
}}
>
{collapsed && (
<MenuItem sx={{ cursor: "default", minWidth: "50%" }}>
<Box
mb={theme.spacing(2)}
sx={{
minWidth: "50%",
maxWidth: "max-content",
overflow: "visible",
whiteSpace: "nowrap",
}}
>
<Typography
component="span"
fontWeight={500}
fontSize={13}
sx={{
display: "block",
whiteSpace: "nowrap",
overflow: "visible",
// wordBreak: "break-word",
textOverflow: "clip",
}}
>
{authState.user?.firstName} {authState.user?.lastName}
</Typography>
<Typography
sx={{
textTransform: "capitalize",
fontSize: 12,
whiteSpace: "nowrap",
overflow: "visible",
// wordBreak: "break-word",
}}
>
{authState.user?.role}
</Typography>
</Box>
</MenuItem>
)}
{/* TODO Do we need two dividers? */}
{collapsed && <Divider />}
{/* <Divider /> */}
{renderAccountMenuItems()}
<MenuItem
onClick={logout}
sx={{
gap: theme.spacing(4),
borderRadius: theme.shape.borderRadius,
pl: theme.spacing(4),
"& svg path": {
stroke: iconColor,
},
}}
>
<LogoutSvg />
{t("menu.logOut", "Log out")}
</MenuItem>
</Menu>
</Stack>
<AuthFooter
collapsed={collapsed}
accountMenuItems={accountMenuItems}
/>
</Stack>
);
}
};
export default Sidebar;
+1 -1
View File
@@ -74,7 +74,7 @@ const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" })
<Typography
variant="body1"
sx={{
color: theme.palette.text.secondary,
color: theme.palette.primary.contrastTextTertiary,
fontSize: "0.938rem",
lineHeight: 1.5,
mb: 1,
+1
View File
@@ -16,6 +16,7 @@ const CustomTabList = ({ value, onChange, children, ...props }) => {
return (
<Box
sx={{
marginBottom: theme.spacing(12),
borderBottom: `1px solid ${theme.palette.primary.lowContrast}`,
"& .MuiTabs-root": { height: "fit-content", minHeight: "0" },
}}
@@ -1,9 +1,7 @@
import PropTypes from "prop-types";
import { Box, Button } from "@mui/material";
import LeftArrowDouble from "../../../../assets/icons/left-arrow-double.svg?react";
import RightArrowDouble from "../../../../assets/icons/right-arrow-double.svg?react";
import LeftArrow from "../../../../assets/icons/left-arrow.svg?react";
import RightArrow from "../../../../assets/icons/right-arrow.svg?react";
import LeftArrow from "../../../ArrowLeft";
import RightArrow from "../../../ArrowRight";
import { useTheme } from "@emotion/react";
TablePaginationActions.propTypes = {
@@ -50,7 +48,7 @@ function TablePaginationActions({ count, page, rowsPerPage, onPageChange }) {
disabled={page === 0}
aria-label="first page"
>
<LeftArrowDouble />
<LeftArrow type="double" />
</Button>
<Button
variant="group"
@@ -74,7 +72,7 @@ function TablePaginationActions({ count, page, rowsPerPage, onPageChange }) {
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="last page"
>
<RightArrowDouble />
<RightArrow type="double" />
</Button>
</Box>
);
+51 -16
View File
@@ -7,6 +7,7 @@ import {
TableHead,
TableRow,
} from "@mui/material";
import Tooltip from "@mui/material/Tooltip";
import SkeletonLayout from "./skeleton";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
@@ -41,6 +42,7 @@ const DataTable = ({
data = [],
config = {
emptyView: "No data",
tooltipContent: null,
onRowClick: () => {},
},
}) => {
@@ -70,6 +72,9 @@ const DataTable = ({
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastTextSecondary,
},
"& .MuiTableBody-root .MuiTableRow-root:last-child .MuiTableCell-root": {
borderBottom: "none",
},
}}
>
<TableHead>
@@ -98,24 +103,54 @@ const DataTable = ({
data.map((row) => {
const key = row.id || row._id || Math.random();
return (
<TableRow
<Tooltip
key={key}
sx={config?.rowSX ?? {}}
onClick={config?.onRowClick ? () => config.onRowClick(row) : null}
followCursor
enterDelay={500}
enterNextDelay={500}
title={
typeof config.tooltipContent === "function"
? config.tooltipContent(row)
: config.tooltipContent
}
slotProps={{
tooltip: {
sx: {
background: "unset",
},
},
popper: {
modifiers: [
{
name: "offset",
options: {
offset: ({ popper }) => {
return [popper.width / 2 + 20, -popper.height / 8];
},
},
},
],
},
}}
>
{headers.map((header, index) => {
return (
<TableCell
align={index === 0 ? "left" : "center"}
key={header.id}
onClick={header.onClick ? (e) => header.onClick(e, row) : null}
sx={header.getCellSx ? header.getCellSx(row) : {}}
>
{header.render(row)}
</TableCell>
);
})}
</TableRow>
<TableRow
sx={config?.rowSX ?? {}}
onClick={config?.onRowClick ? () => config.onRowClick(row) : null}
>
{headers.map((header, index) => {
return (
<TableCell
align={index === 0 ? "left" : "center"}
key={header.id}
onClick={header.onClick ? (e) => header.onClick(e, row) : null}
sx={header.getCellSx ? header.getCellSx(row) : {}}
>
{header.render(row)}
</TableCell>
);
})}
</TableRow>
</Tooltip>
);
})
)}
+34
View File
@@ -0,0 +1,34 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
const ToastBody = ({ body }) => {
const theme = useTheme();
if (Array.isArray(body)) {
return (
<Stack gap={theme.spacing(2)}>
{body.map((item, idx) => (
<Typography
key={`item-${idx}`}
color={theme.palette.secondary.contrastText}
>
{item}
</Typography>
))}
</Stack>
);
} else if (typeof body === "string") {
return <Typography color={theme.palette.secondary.contrastText}>{body}</Typography>;
}
return null;
};
ToastBody.propTypes = {
body: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
};
export default ToastBody;
+96
View File
@@ -0,0 +1,96 @@
import Stack from "@mui/material/Stack";
import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import ToastBody from "./body";
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined";
import CloseIcon from "@mui/icons-material/Close";
// Utils
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
const icons = {
info: <InfoOutlinedIcon />,
error: <ErrorOutlineOutlinedIcon />,
warning: <WarningAmberOutlinedIcon />,
};
const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => {
const theme = useTheme();
const icon = icons[variant];
return (
<Stack
gap={theme.spacing(2)}
paddingTop={theme.spacing(4)}
paddingRight={theme.spacing(8)}
paddingBottom={theme.spacing(4)}
paddingLeft={theme.spacing(8)}
backgroundColor={theme.palette.alert.main}
border={`solid 1px ${theme.palette.alert.contrastText}`}
borderRadius={theme.shape.borderRadius}
>
<Stack
direction="row"
gap={theme.spacing(8)}
justifyContent="space-between"
alignItems="center"
>
{hasIcon && icon}
{title && (
<Typography
fontWeight="700"
color={theme.palette.secondary.contrastText}
>
{title}
</Typography>
)}
{title && (
<IconButton onClick={onClick}>
<CloseIcon />
</IconButton>
)}
</Stack>
<Stack
direction="row"
gap={theme.spacing(2)}
alignItems="center"
>
<ToastBody body={body} />
{!title && (
<IconButton onClick={onClick}>
<CloseIcon />
</IconButton>
)}
</Stack>
{hasDismiss && (
<Button
variant="text"
color="info"
onClick={onClick}
sx={{
fontWeight: "600",
width: "fit-content",
}}
>
Dismiss
</Button>
)}
</Stack>
);
};
export default Toast;
Toast.propTypes = {
variant: PropTypes.string.isRequired,
title: PropTypes.string,
body: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
hasDismiss: PropTypes.bool,
hasIcon: PropTypes.bool,
onClick: PropTypes.func,
};
+1 -5
View File
@@ -44,7 +44,6 @@ export const login = createAsyncThunk("auth/login", async (form, thunkApi) => {
export const update = createAsyncThunk("auth/update", async (data, thunkApi) => {
const { localData: form } = data;
const user = thunkApi.getState().auth.user;
try {
const fd = new FormData();
form.firstName && fd.append("firstName", form.firstName);
@@ -60,7 +59,6 @@ export const update = createAsyncThunk("auth/update", async (data, thunkApi) =>
form.deleteProfileImage && fd.append("deleteProfileImage", form.deleteProfileImage);
const res = await networkService.updateUser({
userId: user._id,
form: fd,
});
return res.data;
@@ -77,10 +75,8 @@ export const update = createAsyncThunk("auth/update", async (data, thunkApi) =>
});
export const deleteUser = createAsyncThunk("auth/delete", async (_, thunkApi) => {
const user = thunkApi.getState().auth.user;
try {
const res = await networkService.deleteUser({ userId: user._id });
const res = await networkService.deleteUser();
return res.data;
} catch (error) {
if (error.response && error.response.data) {
+14 -7
View File
@@ -185,23 +185,30 @@ const useResolveIncident = () => {
return [resolveIncident, isLoading];
};
const useAckAllChecks = () => {
const useAcknowledgeChecks = () => {
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const ackAllChecks = async (setUpdateTrigger) => {
const acknowledge = async (setUpdateTrigger, monitorId = null) => {
setIsLoading(true);
try {
setIsLoading(true);
await networkService.updateAllChecksStatus({ ack: true });
if (monitorId) {
await networkService.updateMonitorChecksStatus({ monitorId, ack: true });
} else {
await networkService.updateAllChecksStatus({ ack: true });
}
setUpdateTrigger((prev) => !prev);
} catch (error) {
createToast({ body: t("checkHooks.failureResolveAll") });
const toastMessage = monitorId
? t("checkHooks.failureResolveMonitor")
: t("checkHooks.failureResolveAll");
createToast({ body: toastMessage });
} finally {
setIsLoading(false);
}
};
return [ackAllChecks, isLoading];
return { acknowledge, isLoading };
};
export {
@@ -209,5 +216,5 @@ export {
useFetchChecksTeam,
useFetchChecksSummaryByTeamId,
useResolveIncident,
useAckAllChecks,
useAcknowledgeChecks,
};
+31 -12
View File
@@ -14,25 +14,25 @@ const useGetInviteToken = () => {
const clearToken = () => {
setToken(undefined);
};
const fetchToken = async (email, role) => {
const response = await networkService.requestInvitationToken({ email, role });
const token = response?.data?.data?.token;
if (typeof token === "undefined") {
throw new Error(t("inviteNoTokenFound"));
}
return token;
};
const getInviteToken = async ({ email, role }) => {
try {
const response = await networkService.requestInvitationToken({
email,
role,
});
const token = response?.data?.data?.token;
if (typeof token === "undefined") {
throw new Error(t("inviteNoTokenFound"));
}
setIsLoading(true);
const token = await fetchToken(email, role);
let inviteLink = token;
if (typeof CLIENT_HOST !== "undefined") {
inviteLink = `${CLIENT_HOST}/register/${token}`;
}
setToken(inviteLink);
return token;
} catch (error) {
setError(error);
} finally {
@@ -40,7 +40,26 @@ const useGetInviteToken = () => {
}
};
return [getInviteToken, clearToken, isLoading, error, token];
const addTeamMember = async (formData, role) => {
try {
setIsLoading(true);
const token = await fetchToken(formData.email, role);
const toSubmit = {
...formData,
inviteToken: token,
};
delete toSubmit.confirm;
const responseRegister = await networkService.registerUser(toSubmit);
return responseRegister;
} catch (error) {
setError(error);
throw error;
} finally {
setIsLoading(false);
}
};
return [getInviteToken, clearToken, isLoading, error, token, addTeamMember];
};
export { useGetInviteToken };
+80 -4
View File
@@ -15,9 +15,6 @@ const useFetchLogs = () => {
setIsLoading(true);
const response = await networkService.getLogs();
setLogs(response.data.data);
createToast({
body: t("logsPage.toast.fetchLogsSuccess"),
});
} catch (error) {
setError(error);
createToast({
@@ -32,4 +29,83 @@ const useFetchLogs = () => {
return [logs, isLoading, error];
};
export { useFetchLogs };
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];
};
const useFetchDiagnostics = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
const [diagnostics, setDiagnostics] = useState(undefined);
const fetchDiagnostics = async () => {
try {
setIsLoading(true);
const response = await networkService.getDiagnostics();
setDiagnostics(response.data.data);
} catch (error) {
createToast({
body: error.message,
});
setError(error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchDiagnostics();
}, []);
return [diagnostics, fetchDiagnostics, isLoading, error];
};
export { useFetchLogs, useFetchQueueData, useFlushQueue, useFetchDiagnostics };
+51 -1
View File
@@ -202,6 +202,25 @@ const useFetchStatsByMonitorId = ({
return [monitor, audits, isLoading, networkError];
};
const useFetchMonitorGames = ({ setGames, updateTrigger }) => {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchGames = async () => {
try {
setIsLoading(true);
const res = await networkService.getMonitorGames();
setGames(res.data.data);
} catch (error) {
createToast({ body: error.message });
} finally {
setIsLoading(false);
}
};
fetchGames();
}, [setGames, updateTrigger]);
return [isLoading];
};
const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
@@ -300,6 +319,28 @@ const useCreateMonitor = () => {
return [createMonitor, isLoading];
};
const useFetchGlobalSettings = () => {
const [isLoading, setIsLoading] = useState(true);
const [globalSettings, setGlobalSettings] = useState(undefined);
useEffect(() => {
const fetchGlobalSettings = async () => {
try {
const res = await networkService.getAppSettings();
setGlobalSettings(res?.data);
} catch (error) {
console.error("Failed to fetch global settings:", error);
createToast({ body: "Failed to load global settings" });
} finally {
setIsLoading(false);
}
};
fetchGlobalSettings();
}, []);
return [globalSettings, isLoading];
};
const useDeleteMonitor = () => {
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
@@ -328,6 +369,8 @@ const useUpdateMonitor = () => {
setIsLoading(true);
const updatedFields = {
name: monitor.name,
statusWindowSize: monitor.statusWindowSize,
statusWindowThreshold: monitor.statusWindowThreshold,
description: monitor.description,
interval: monitor.interval,
notifications: monitor.notifications,
@@ -335,7 +378,12 @@ const useUpdateMonitor = () => {
expectedValue: monitor.expectedValue,
ignoreTlsErrors: monitor.ignoreTlsErrors,
jsonPath: monitor.jsonPath,
...(monitor.type === "port" && { port: monitor.port }),
...((monitor.type === "port" || monitor.type === "game") && {
port: monitor.port,
}),
...(monitor.type == "game" && {
gameId: monitor.gameId,
}),
...(monitor.type === "hardware" && {
thresholds: monitor.thresholds,
secret: monitor.secret,
@@ -499,6 +547,7 @@ export {
useFetchUptimeMonitorById,
useFetchHardwareMonitorById,
useCreateMonitor,
useFetchGlobalSettings,
useDeleteMonitor,
useUpdateMonitor,
usePauseMonitor,
@@ -507,4 +556,5 @@ export {
useDeleteMonitorStats,
useCreateBulkMonitors,
useExportMonitors,
useFetchMonitorGames,
};
-57
View File
@@ -1,57 +0,0 @@
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 };
+7 -1
View File
@@ -8,4 +8,10 @@ const useIsAdmin = () => {
return isAdmin;
};
export { useIsAdmin };
const useIsSuperAdmin = () => {
const { user } = useSelector((state) => state.auth);
const isSuperAdmin = user?.role?.includes("superadmin");
return isSuperAdmin;
};
export { useIsAdmin, useIsSuperAdmin };
+7 -7
View File
@@ -6,18 +6,18 @@ const useMonitorUtils = () => {
let uptimePercentage = "";
let percentageColor = "";
if (monitor.uptimePercentage !== undefined) {
if (monitor?.uptimePercentage !== undefined) {
uptimePercentage =
monitor.uptimePercentage === 0
monitor?.uptimePercentage === 0
? "0"
: (monitor.uptimePercentage * 100).toFixed(2);
: (monitor?.uptimePercentage * 100).toFixed(2);
percentageColor =
monitor.uptimePercentage < 0.25
monitor?.uptimePercentage < 0.25
? theme.palette.error.main
: monitor.uptimePercentage < 0.5
: monitor?.uptimePercentage < 0.5
? theme.palette.warning.main
: monitor.uptimePercentage < 0.75
: monitor?.uptimePercentage < 0.75
? theme.palette.success.main
: theme.palette.success.main;
}
@@ -32,7 +32,7 @@ const useMonitorUtils = () => {
const determineState = useCallback((monitor) => {
if (typeof monitor === "undefined") return "pending";
if (monitor.isActive === false) return "paused";
if (monitor?.isActive === false) return "paused";
if (monitor?.status === undefined) return "pending";
return monitor?.status == true ? "up" : "down";
}, []);
+1 -1
View File
@@ -34,7 +34,7 @@ const useCreateNotification = () => {
const useGetNotificationsByTeamId = (updateTrigger) => {
const [notifications, setNotifications] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const { t } = useTranslation();
+61
View File
@@ -0,0 +1,61 @@
import { useEffect, useState, useCallback } from "react";
import { networkService } from "../main";
import { createToast } from "../Utils/toastUtils";
import { useTranslation } from "react-i18next";
export const useGetUser = (userId) => {
const [user, setUser] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const fetchUser = useCallback(async () => {
try {
setIsLoading(true);
const response = await networkService.getUserById({ userId });
setUser(response?.data?.data);
} catch (error) {
createToast({
body: error.message,
});
setError(error);
} finally {
setIsLoading(false);
}
}, [userId]);
useEffect(() => {
if (userId) {
fetchUser();
}
}, [userId, fetchUser]);
return [user, isLoading, error];
};
export const useEditUser = (userId) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const { t } = useTranslation();
const editUser = useCallback(
async (user) => {
try {
setIsLoading(true);
await networkService.editUser({ userId, user });
createToast({
body: t("editUserPage.toast.successUserUpdate"),
});
} catch (error) {
createToast({
body: error.message,
});
setError(error);
} finally {
setIsLoading(false);
}
},
[userId]
);
return [editUser, isLoading, error];
};
@@ -0,0 +1,102 @@
import { useState, useEffect } from "react";
import { ROLES } from "../../../../Utils/roleUtils";
import { editUserValidation } from "../../../../Validation/validation";
import Joi from "joi";
import { createToast } from "../../../../Utils/toastUtils";
import { useTranslation } from "react-i18next";
export const useEditUserForm = (user) => {
const [searchInput, setSearchInput] = useState("");
const [form, setForm] = useState({
firstName: "",
lastName: "",
email: "",
role: [],
});
// Effect to set user
useEffect(() => {
if (user) {
setForm({
firstName: user?.firstName,
lastName: user?.lastName,
email: user?.email,
role: user?.role,
});
}
}, [user]);
const handleRoleChange = (value) => {
const hasSuperAdmin = form.role.includes(ROLES.SUPERADMIN);
const newRoles = value.map((item) => item.role);
if (hasSuperAdmin && !newRoles.includes(ROLES.SUPERADMIN)) {
newRoles.push(ROLES.SUPERADMIN);
}
setForm({
...form,
role: newRoles,
});
};
const handleDeleteRole = (role) => {
if (role === ROLES.SUPERADMIN) return;
setForm({ ...form, role: form?.role?.filter((r) => r !== role) });
};
const handleSearchInput = (value) => {
setSearchInput(value);
};
return [
form,
setForm,
handleRoleChange,
handleDeleteRole,
searchInput,
handleSearchInput,
];
};
export const useValidateEditUserForm = () => {
const [errors, setErrors] = useState({});
const { t } = useTranslation();
const validateForm = (form) => {
const { error } = editUserValidation.validate(form, {
abortEarly: false,
});
const errs = error?.details;
const errsObj = errs?.reduce((acc, curr) => {
acc[curr.path[0]] = curr.message;
return acc;
}, {});
setErrors(errsObj);
if (errs?.length > 0) {
createToast({
variant: "warning",
hasIcon: true,
title: t("editUserPage.toast.validationErrors"),
body: errs.map((err) => t(err.message)),
});
return false;
}
return true;
};
const validateField = (name, value) => {
const fieldSchema = Joi.object({
[name]: editUserValidation.extract(name),
});
const { error } = fieldSchema.validate({ [name]: value });
setErrors((prev) => {
const prevErrors = { ...prev };
if (error) prevErrors[name] = error.details[0].message;
else delete prevErrors[name];
return prevErrors;
});
};
return [errors, validateForm, validateField];
};
+122
View File
@@ -0,0 +1,122 @@
// Components
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import TextInput from "../../../Components/Inputs/TextInput";
import Search from "../../../Components/Inputs/Search";
import Button from "@mui/material/Button";
import RoleTable from "../components/RoleTable";
// Utils
import { useParams } from "react-router-dom";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useGetUser, useEditUser } from "../../../Hooks/userHooks";
import { EDITABLE_ROLES, ROLES } from "../../../Utils/roleUtils";
import { useEditUserForm, useValidateEditUserForm } from "./hooks/editUser";
const EditUser = () => {
const { userId } = useParams();
const theme = useTheme();
const { t } = useTranslation();
const BREADCRUMBS = [
{ name: t("menu.team"), path: "/account/team" },
{ name: t("editUserPage.title"), path: "" },
];
const [user, isLoading, error] = useGetUser(userId);
const [editUser, isSaving, saveError] = useEditUser(userId);
const [
form,
setForm,
handleRoleChange,
handleDeleteRole,
searchInput,
handleSearchInput,
] = useEditUserForm(user);
const [errors, validateForm, validateField] = useValidateEditUserForm();
const onChange = (e) => {
const name = e.target.name;
const value = e.target.value;
validateField(name, value);
setForm({ ...form, [name]: value });
};
const handleSubmit = (e) => {
e.preventDefault();
const valid = validateForm(form);
if (valid) {
editUser(form);
}
};
return (
<Stack gap={theme.spacing(20)}>
<Breadcrumbs list={BREADCRUMBS} />
<Typography variant="h2">{t("editUserPage.title")}</Typography>
<Stack
component="form"
onSubmit={handleSubmit}
gap={theme.spacing(12)}
maxWidth="50%"
>
<TextInput
name="firstName"
label={t("editUserPage.form.firstName")}
value={form?.firstName}
onChange={onChange}
error={errors?.firstName ? true : false}
helperText={t(errors?.firstName)}
/>
<TextInput
name="lastName"
label={t("editUserPage.form.lastName")}
value={form?.lastName}
onChange={onChange}
error={errors?.lastName ? true : false}
helperText={t(errors?.lastName)}
/>
<TextInput
name="email"
label={t("editUserPage.form.email")}
value={form?.email}
disabled={true}
error={errors?.email ? true : false}
helperText={t(errors?.email)}
/>
<Search
label={t("editUserPage.form.role")}
filteredBy="role"
inputValue={searchInput}
handleInputChange={handleSearchInput}
value={
form?.role
?.filter((role) => role !== ROLES.SUPERADMIN)
.map((role) => ({ role, _id: role })) || []
}
options={EDITABLE_ROLES}
multiple={true}
handleChange={handleRoleChange}
/>
<RoleTable
roles={form?.role}
handleDeleteRole={handleDeleteRole}
/>
<Box>
<Button
type="submit"
variant="contained"
color="accent"
loading={isLoading || isSaving}
>
{t("editUserPage.form.save")}
</Button>
</Box>
</Stack>
</Stack>
);
};
export default EditUser;
@@ -0,0 +1,68 @@
import { useState } from "react";
import Button from "@mui/material/Button";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import { useTheme } from "@emotion/react";
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import { useTranslation } from "react-i18next";
import Proptypes from "prop-types";
const AddMemberMenu = ({ handleInviteOpen, handleIsRegisterOpen }) => {
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const { t } = useTranslation();
const theme = useTheme();
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
return (
<>
<Button
variant="contained"
color="accent"
endIcon={<ArrowDropDownIcon sx={{ color: theme.palette.secondary.light }} />}
onClick={handleClick}
>
{t("teamPanel.addTeamMember.addMemberMenu")}
</Button>
<Menu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
sx={{
"& .MuiPaper-root": {
minWidth: anchorEl?.offsetWidth || "auto",
},
}}
>
<MenuItem
onClick={() => {
handleClose();
handleInviteOpen();
}}
>
{t("teamPanel.inviteTeamMember")}
</MenuItem>
<MenuItem
onClick={() => {
handleClose();
handleIsRegisterOpen(true);
}}
>
{t("teamPanel.register")}
</MenuItem>
</Menu>
</>
);
};
AddMemberMenu.propTypes = {
handleInviteOpen: Proptypes.func.isRequired,
handleIsRegisterOpen: Proptypes.func.isRequired,
};
export default AddMemberMenu;
@@ -0,0 +1,48 @@
import { useState } from "react";
import { newOrChangedCredentials } from "../../../../../Validation/validation";
import { useTranslation } from "react-i18next";
const useAddTeamMember = () => {
const { t } = useTranslation();
const [errors, setErrors] = useState({});
const clearErrors = () => setErrors({});
const validateFields = (name, value, formData) => {
const { error } = newOrChangedCredentials.validate(
{ [name]: value },
{ abortEarly: false, context: { password: formData.password } }
);
setErrors((prev) => ({
...prev,
[name]: error?.details?.[0]?.message || "",
}));
};
const validateForm = (formData, role) => {
const { error } = newOrChangedCredentials.validate(formData, {
abortEarly: false,
context: { password: formData.password },
});
const formErrors = {};
if (error) {
for (const err of error.details) {
formErrors[err.path[0]] = err.message;
}
}
if (!role[0] || role.length === 0) {
formErrors.role = t(
"teamPanel.registerTeamMember.auth.common.inputs.role.errors.empty"
);
}
if (Object.keys(formErrors).length > 0) {
setErrors(formErrors);
return false;
}
setErrors({});
return true;
};
return { errors, setErrors, clearErrors, validateFields, validateForm };
};
export default useAddTeamMember;
@@ -0,0 +1,213 @@
import { Button, Stack } from "@mui/material";
import { GenericDialog } from "../../../../Components/Dialog/genericDialog";
import TextInput from "../../../../Components/Inputs/TextInput";
import Select from "../../../../Components/Inputs/Select";
import { useGetInviteToken } from "../../../../Hooks/inviteHooks";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { createToast } from "../../../../Utils/toastUtils";
import { useState } from "react";
import PasswordTooltip from "../../../Auth/components/PasswordTooltip";
import useAddTeamMember from "./hooks/useAddTeamMember";
import usePasswordFeedback from "../../../Auth/hooks/usePasswordFeedback";
import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments";
import PropTypes from "prop-types";
const INITIAL_FORM_STATE = {
firstName: "",
lastName: "",
email: "",
password: "",
confirm: "",
teamId: "",
};
const INITIAL_ROLE_STATE = ["user"];
const AddTeamMember = ({ handleIsRegisterOpen, isRegisterOpen, onMemberAdded }) => {
const theme = useTheme();
const { t } = useTranslation();
const { errors, setErrors, clearErrors, validateFields, validateForm } =
useAddTeamMember();
const { feedback, handlePasswordFeedback } = usePasswordFeedback();
const [getInviteToken, clearToken, isLoading, error, token, addTeamMember] =
useGetInviteToken();
const [form, setForm] = useState(INITIAL_FORM_STATE);
const [role, setRole] = useState(INITIAL_ROLE_STATE);
const [isLoadingSubmit, setIsLoadingSubmit] = useState(false);
const closeAddMemberModal = () => {
handleIsRegisterOpen(false);
setForm(INITIAL_FORM_STATE);
setRole(INITIAL_ROLE_STATE);
clearErrors();
clearToken();
};
const onChange = (e) => {
let { name, value } = e.target;
if (name === "email") value = value.toLowerCase();
const updatedForm = { ...form, [name]: value };
validateFields(name, value, updatedForm);
setForm(updatedForm);
if (name === "password" || name === "confirm") {
handlePasswordFeedback(updatedForm, name, value, form, errors, setErrors);
}
};
const onsubmitAddMember = async (event) => {
event.preventDefault();
if (!validateForm(form, role)) return;
try {
setIsLoadingSubmit(true);
await addTeamMember(form, role);
createToast({
body: t("teamPanel.registerToast.success"),
});
onMemberAdded();
closeAddMemberModal();
} catch (error) {
const errorMsg = error.response?.data?.msg || error.message || "unknownError";
createToast({
type: "error",
body: t(errorMsg),
});
} finally {
setIsLoadingSubmit(false);
}
};
const tErr = (key) => (key ? t([`teamPanel.registerTeamMember.${key}`, key]) : "");
return (
<>
<GenericDialog
title={t("teamPanel.addTeamMember.title")}
description={t("teamPanel.addTeamMember.description")}
open={isRegisterOpen}
onClose={closeAddMemberModal}
theme={theme}
width={{ sm: "55%", md: "50%", lg: "40%", xl: "30%" }}
>
<TextInput
name="firstName"
label={t("auth.common.inputs.firstName.label")}
isRequired={true}
gap={theme.spacing(4)}
placeholder={t("auth.common.inputs.firstName.placeholder")}
value={form.firstName}
onChange={onChange}
error={errors.firstName ? true : false}
helperText={errors.firstName ? tErr(errors.firstName) : null}
sx={{ mb: errors.firstName ? theme.spacing(15) : theme.spacing(5) }}
/>
<TextInput
name="lastName"
label={t("auth.common.inputs.lastName.label")}
isRequired={true}
gap={theme.spacing(4)}
placeholder={t("auth.common.inputs.lastName.placeholder")}
value={form.lastName}
onChange={onChange}
error={errors.lastName ? true : false}
helperText={errors.lastName ? tErr(errors.lastName) : null}
sx={{ mb: errors.lastName ? theme.spacing(15) : theme.spacing(5) }}
/>
<TextInput
type="email"
label={t("auth.common.inputs.email.label")}
name="email"
gap={theme.spacing(4)}
isRequired={true}
id="input-team-member"
placeholder={t("teamPanel.email")}
value={form.email}
onChange={onChange}
error={errors.email ? true : false}
helperText={errors.email ? tErr(errors.email) : null}
sx={{ mb: errors.email ? theme.spacing(15) : theme.spacing(5) }}
/>
<Select
label={t("teamPanel.role")}
id="team-member-role"
name="role"
required={true}
placeholder={t("teamPanel.selectRole")}
isHidden={true}
value={role[0] || ""}
sx={{ mb: theme.spacing(5) }}
onChange={(event) => setRole([event.target.value])}
items={[
{ _id: "admin", name: t("roles.admin") },
{ _id: "user", name: t("roles.teamMember") },
]}
/>
<PasswordTooltip
feedback={feedback}
form={form}
>
<TextInput
type="password"
id="register-password-input"
name="password"
label={t("auth.common.inputs.password.label")}
isRequired={true}
placeholder="••••••••••"
value={form.password}
onChange={onChange}
error={errors.password && errors.password[0] ? true : false}
endAdornment={<PasswordEndAdornment />}
sx={{ mb: theme.spacing(5) }}
/>
</PasswordTooltip>
<TextInput
type="password"
id="register-confirm-input"
name="confirm"
label={t("auth.common.inputs.passwordConfirm.label")}
gap={theme.spacing(4)}
isRequired={true}
placeholder={t("auth.common.inputs.passwordConfirm.placeholder")}
autoComplete="current-password"
value={form.confirm}
onChange={onChange}
error={errors.confirm && errors.confirm[0] ? true : false}
endAdornment={<PasswordEndAdornment />}
sx={{ mb: theme.spacing(5) }}
/>
<Stack
direction="row"
spacing={theme.spacing(10)}
mt={theme.spacing(8)}
justifyContent="flex-end"
>
<Button
variant="contained"
color="error"
onClick={closeAddMemberModal}
disabled={isLoadingSubmit}
>
{t("teamPanel.cancel")}
</Button>
<Button
variant="contained"
color="accent"
onClick={onsubmitAddMember}
disabled={isLoadingSubmit}
>
{t("teamPanel.addTeamMember.addButton")}
</Button>
</Stack>
</GenericDialog>
</>
);
};
AddTeamMember.propTypes = {
handleIsRegisterOpen: PropTypes.func.isRequired,
isRegisterOpen: PropTypes.bool.isRequired,
onMemberAdded: PropTypes.func.isRequired,
};
export default AddTeamMember;
@@ -2,10 +2,10 @@ import TabPanel from "@mui/lab/TabPanel";
import { useState } from "react";
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 { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments";
import TextInput from "../../../Components/Inputs/TextInput";
import { newOrChangedCredentials } from "../../../Validation/validation";
import Alert from "../../Alert";
import Alert from "../../../Components/Alert";
import { update } from "../../../Features/Auth/authSlice";
import { useDispatch, useSelector } from "react-redux";
import { createToast } from "../../../Utils/toastUtils";
@@ -2,16 +2,16 @@ import { useTheme } from "@emotion/react";
import { useState } from "react";
import TabPanel from "@mui/lab/TabPanel";
import { Box, Button, Divider, Stack, Typography } from "@mui/material";
import Avatar from "../../Avatar";
import TextInput from "../../Inputs/TextInput";
import ImageUpload from "../../Inputs/ImageUpload";
import Avatar from "../../../Components/Avatar";
import TextInput from "../../../Components/Inputs/TextInput";
import ImageUpload from "../../../Components/Inputs/ImageUpload";
import { newOrChangedCredentials } from "../../../Validation/validation";
import { useDispatch, useSelector } from "react-redux";
import { clearAuthState, deleteUser, update } from "../../../Features/Auth/authSlice";
import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
import { GenericDialog } from "../../Dialog/genericDialog";
import Dialog from "../../Dialog";
import { GenericDialog } from "../../../Components/Dialog/genericDialog";
import Dialog from "../../../Components/Dialog";
import { useTranslation } from "react-i18next";
/**
@@ -191,7 +191,6 @@ const ProfilePanel = () => {
>
<Stack
component="form"
className="edit-profile-form"
noValidate
spellCheck="false"
gap={SPACING_GAP}
@@ -0,0 +1,42 @@
import Typography from "@mui/material/Typography";
import DataTable from "../../../../Components/Table";
import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
import { useTranslation } from "react-i18next";
import { ROLES } from "../../../../Utils/roleUtils";
const RoleTable = ({ roles, handleDeleteRole }) => {
const { t } = useTranslation();
const HEADERS = [
{
id: "name",
content: <Typography>{t("editUserPage.table.roleHeader")}</Typography>,
render: (row) => {
return row;
},
},
{
id: "delete",
content: <Typography>{t("editUserPage.table.actionHeader")}</Typography>,
render: (row) => {
if (row === ROLES.SUPERADMIN) return null;
return (
<DeleteOutlineRoundedIcon
onClick={() => {
handleDeleteRole(row);
}}
sx={{ cursor: "pointer" }}
/>
);
},
},
];
return (
<DataTable
headers={HEADERS}
data={roles}
/>
);
};
export default RoleTable;
@@ -3,14 +3,18 @@ import TabPanel from "@mui/lab/TabPanel";
import { Button, ButtonGroup, Stack, Typography } from "@mui/material";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import TextInput from "../../Inputs/TextInput";
import TextInput from "../../../Components/Inputs/TextInput";
import { newOrChangedCredentials } from "../../../Validation/validation";
import { networkService } from "../../../main";
import { createToast } from "../../../Utils/toastUtils";
import Select from "../../Inputs/Select";
import { GenericDialog } from "../../Dialog/genericDialog";
import DataTable from "../../Table/";
import Select from "../../../Components/Inputs/Select";
import { GenericDialog } from "../../../Components/Dialog/genericDialog";
import AddTeamMember from "../components/AddTeamMember";
import DataTable from "../../../Components/Table";
import { useGetInviteToken } from "../../../Hooks/inviteHooks";
import { useNavigate } from "react-router-dom";
import { useIsSuperAdmin } from "../../../Hooks/useIsAdmin";
import AddMemberMenu from "./AddMemberMenu";
/**
* TeamPanel component manages the organization and team members,
* providing functionalities like renaming the organization, managing team members,
@@ -34,8 +38,9 @@ const TeamPanel = () => {
const [isDisabled, setIsDisabled] = useState(true);
const [errors, setErrors] = useState({});
const [isSendingInvite, setIsSendingInvite] = useState(false);
const navigate = useNavigate();
const [getInviteToken, clearToken, isLoading, error, token] = useGetInviteToken();
const isSuperAdmin = useIsSuperAdmin();
const headers = [
{
@@ -62,7 +67,10 @@ const TeamPanel = () => {
render: (row) => row.role,
},
];
const [refreshTrigger, setRefreshTrigger] = useState(false);
const refreshTeamList = () => {
setRefreshTrigger((prev) => !prev);
};
useEffect(() => {
const fetchTeam = async () => {
try {
@@ -76,7 +84,7 @@ const TeamPanel = () => {
};
fetchTeam();
}, []);
}, [refreshTrigger]);
useEffect(() => {
const ROLE_MAP = {
@@ -106,6 +114,10 @@ const TeamPanel = () => {
setIsDisabled(Object.keys(errors).length !== 0 || toInvite.email === "");
}, [errors, toInvite.email]);
const [isOpen, setIsOpen] = useState(false);
const [isRegisterOpen, setIsRegisterOpen] = useState(false);
const handleIsRegisterOpen = (open) => {
setIsRegisterOpen(open);
};
const handleChange = (event) => {
const { value } = event.target;
@@ -135,7 +147,6 @@ const TeamPanel = () => {
const handleGetToken = async () => {
await getInviteToken({ email: toInvite.email, role: toInvite.role });
};
const handleInviteMember = async () => {
if (!toInvite.email) {
setErrors((prev) => ({ ...prev, email: "Email is required." }));
@@ -184,7 +195,6 @@ const TeamPanel = () => {
return (
<TabPanel
className="team-panel table-container"
value="team"
sx={{
"& h1": {
@@ -237,22 +247,34 @@ const TeamPanel = () => {
</Button>
</ButtonGroup>
</Stack>
<Button
variant="contained"
color="accent"
onClick={() => setIsOpen(true)}
>
{t("teamPanel.inviteTeamMember")}
</Button>
<AddTeamMember
handleIsRegisterOpen={handleIsRegisterOpen}
isRegisterOpen={isRegisterOpen}
onMemberAdded={refreshTeamList}
/>
<AddMemberMenu
handleInviteOpen={() => setIsOpen(true)}
handleIsRegisterOpen={handleIsRegisterOpen}
/>
</Stack>
<DataTable
headers={headers}
data={data}
config={{ emptyView: t("teamPanel.noMembers") }}
config={{
emptyView: t("teamPanel.noMembers"),
rowSX: {
cursor: isSuperAdmin ? "pointer" : "default",
},
onRowClick: (row) => {
if (isSuperAdmin) {
navigate(`/account/team/${row.id}`);
}
},
}}
/>
</Stack>
<GenericDialog
title={t("teamPanel.inviteNewTeamMember")}
description={t("teamPanel.inviteDescription")}
-38
View File
@@ -1,38 +0,0 @@
.account h1.MuiTypography-root,
.account p.MuiTypography-root,
.account input,
.account button,
.account td,
.account .MuiSelect-select {
font-size: var(--env-var-font-size-medium);
}
.account h1.MuiTypography-root {
font-weight: 600;
}
.account .MuiTabPanel-root {
padding: 0;
margin-top: 50px;
}
.account button:not(.MuiIconButton-root) {
min-height: 34px;
}
.account .field {
flex: 1;
}
#modal-delete-account,
#modal-edit-org-name,
#modal-invite-member {
font-size: var(--env-var-font-size-large);
}
.account .MuiStack-root:has(span.MuiTypography-root.input-error) {
position: relative;
}
.account:not(:has(#modal-invite-member)) span.MuiTypography-root.input-error {
position: absolute;
top: 100%;
}
.account .MuiTableBody-root .MuiTableCell-root {
padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2);
}
+5 -5
View File
@@ -5,11 +5,10 @@ import { useSelector } from "react-redux";
import { Box, Tab, useTheme } from "@mui/material";
import CustomTabList from "../../Components/Tab";
import TabContext from "@mui/lab/TabContext";
import ProfilePanel from "../../Components/TabPanels/Account/ProfilePanel";
import PasswordPanel from "../../Components/TabPanels/Account/PasswordPanel";
import TeamPanel from "../../Components/TabPanels/Account/TeamPanel";
import ProfilePanel from "./components/ProfilePanel";
import PasswordPanel from "./components/PasswordPanel";
import TeamPanel from "./components/TeamPanel";
import { useTranslation } from "react-i18next";
import "./index.css";
/**
* Account component renders a settings page with tabs for Profile, Password, and Team settings.
@@ -33,7 +32,9 @@ const Account = ({ open = "profile" }) => {
{ name: t("menu.password"), value: "password" },
{ name: t("menu.team"), value: "team" },
];
const hideTeams = !requiredRoles.some((role) => user.role.includes(role));
if (hideTeams) {
tabList = [
{ name: t("menu.profile"), value: "profile" },
@@ -63,7 +64,6 @@ const Account = ({ open = "profile" }) => {
return (
<Box
className="account"
px={theme.spacing(20)}
py={theme.spacing(12)}
>
@@ -0,0 +1,22 @@
// Hook to avoid double submits and manage loading state
import { useState, useCallback } from "react";
const useLoadingSubmit = () => {
const [submitting, setSubmitting] = useState(false);
const executeSubmit = useCallback(
async (submitFunction) => {
if (submitting) return;
setSubmitting(true);
try {
return await submitFunction();
} finally {
setSubmitting(false);
}
},
[submitting]
);
return { submitting, executeSubmit };
};
export default useLoadingSubmit;
@@ -0,0 +1,19 @@
import { useState } from "react";
const useLoginForm = () => {
const [form, setForm] = useState({
email: "",
password: "",
});
const onChange = (e) => {
let { name, value } = e.target;
if (name === "email") {
value = value.toLowerCase();
}
const updatedForm = { ...form, [name]: value };
setForm(updatedForm);
};
return [form, onChange];
};
export default useLoginForm;
@@ -0,0 +1,40 @@
import { useDispatch } from "react-redux";
import { login } from "../../../../Features/Auth/authSlice";
import { useNavigate } from "react-router-dom";
import { createToast } from "../../../../Utils/toastUtils";
import { useTranslation } from "react-i18next";
const useLoginSubmit = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { t } = useTranslation();
const handleLoginSubmit = async (form, setErrors) => {
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 [handleLoginSubmit];
};
export default useLoginSubmit;
@@ -0,0 +1,31 @@
import { useState } from "react";
import { loginCredentials } from "../../../../Validation/validation";
const useValidateLoginForm = () => {
const [errors, setErrors] = useState({
email: "",
password: "",
});
const validateField = (name, value) => {
const { error } = loginCredentials.validate({ [name]: value });
setErrors((prev) => ({
...prev,
[name]: error?.details?.[0]?.message || "",
}));
};
const validateForm = (form) => {
const { error } = loginCredentials.validate(form, { abortEarly: false });
if (error) {
const formErrors = {};
for (const err of error.details) {
formErrors[err.path[0]] = err.message;
}
setErrors(formErrors);
return false;
}
return true;
};
return [errors, setErrors, validateField, validateForm];
};
export default useValidateLoginForm;
+72 -137
View File
@@ -1,167 +1,102 @@
// 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";
import AuthPageWrapper from "../components/AuthPageWrapper";
// 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";
import useLoginForm from "./hooks/useLoginForm";
import useValidateLoginForm from "./hooks/useValidateLoginForm";
import useLoginSubmit from "./hooks/useLoginSubmit";
import useLoadingSubmit from "./hooks/useLoadingSubmit";
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();
const [form, onChange] = useLoginForm();
const [errors, setErrors, validateField, validateForm] = useValidateLoginForm();
const [handleLoginSubmit] = useLoginSubmit();
const { submitting, executeSubmit } = useLoadingSubmit();
// 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 handleChange = (e) => {
onChange(e);
validateField(e.target.name, e.target.value);
};
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"),
});
}
}
const isValid = validateForm(form);
if (!isValid) return;
await executeSubmit(() => handleLoginSubmit(form, setErrors));
};
return (
<Stack
gap={theme.spacing(10)}
minHeight="100vh"
<AuthPageWrapper
welcome={t("auth.login.welcome")}
heading={t("auth.login.heading")}
>
<AuthHeader />
<Stack
margin="auto"
component="form"
width="100%"
alignItems="center"
gap={theme.spacing(10)}
padding={theme.spacing(8)}
gap={theme.spacing(12)}
onSubmit={onSubmit}
sx={{
width: {
sm: "80%",
md: "70%",
lg: "65%",
xl: "65%",
},
}}
>
<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")}
placeholder={t("auth.common.inputs.email.placeholder")}
autoComplete="email"
value={form.email}
onChange={handleChange}
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")}
placeholder="••••••••••"
autoComplete="current-password"
value={form.password}
onChange={handleChange}
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: "100%", alignSelf: "center", fontWeight: 700 }}
loading={submitting}
>
<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"
/>
Login
</Button>
</Stack>
</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"
/>
</AuthPageWrapper>
);
};
export default Login;
+97 -120
View File
@@ -1,10 +1,10 @@
// 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";
import Box from "@mui/material/Box";
import PasswordTooltip from "../components/PasswordTooltip";
// Utils
import { useTheme } from "@emotion/react";
@@ -16,6 +16,7 @@ import { useParams } from "react-router-dom";
import { networkService } from "../../../main";
import { newOrChangedCredentials } from "../../../Validation/validation";
import { register } from "../../../Features/Auth/authSlice";
import AuthPageWrapper from "../components/AuthPageWrapper";
import { createToast } from "../../../Utils/toastUtils";
import PropTypes from "prop-types";
@@ -195,59 +196,38 @@ const Register = ({ superAdminExists }) => {
};
return (
<Stack
gap={theme.spacing(10)}
minHeight="100vh"
<AuthPageWrapper
heading={t("auth.registration.heading.user")}
welcome={t("auth.registration.welcome")}
>
<AuthHeader />
<Stack
margin="auto"
component="form"
width="100%"
alignItems="center"
gap={theme.spacing(10)}
padding={theme.spacing(8)}
gap={theme.spacing(8)}
onSubmit={onSubmit}
sx={{
width: {
sm: "80%",
},
}}
>
<Typography variant="h1">{t("auth.registration.heading.user")}</Typography>
<Typography variant="h2">
{superAdminExists
? t("auth.registration.description.user")
: t("auth.registration.description.superAdmin")}
</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}
direction={{ xs: "column", lg: "row" }}
justifyContent="space-between"
gap={theme.spacing(4)}
>
<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"
sx={{ flex: 1 }}
label={t("auth.common.inputs.firstName.label")}
width="100%"
gap={theme.spacing(4)}
isRequired={true}
placeholder={t("auth.common.inputs.firstName.placeholder")}
autoComplete="given-name"
@@ -259,6 +239,9 @@ const Register = ({ superAdminExists }) => {
<TextInput
name="lastName"
label={t("auth.common.inputs.lastName.label")}
sx={{ flex: 1 }}
width="100%"
gap={theme.spacing(4)}
isRequired={true}
placeholder={t("auth.common.inputs.lastName.placeholder")}
autoComplete="family-name"
@@ -267,82 +250,76 @@ const Register = ({ superAdminExists }) => {
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>
<TextInput
type="email"
name="email"
gap={theme.spacing(4)}
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
/>
<PasswordTooltip
feedback={feedback}
form={form}
>
<Box>
<TextInput
type="password"
id="register-password-input"
name="password"
label={t("auth.common.inputs.password.label")}
gap={theme.spacing(4)}
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)
: ""
}
/>
</Box>
</PasswordTooltip>
<TextInput
type="password"
id="register-confirm-input"
name="confirm"
label={t("auth.common.inputs.passwordConfirm.label")}
gap={theme.spacing(4)}
isRequired={true}
placeholder={t("auth.common.inputs.passwordConfirm.placeholder")}
autoComplete="current-password"
value={form.confirm}
onChange={onPasswordChange}
marginBottom={theme.spacing(4)}
error={errors.confirm && errors.confirm[0] ? true : false}
/>
<Button
disabled={isLoading}
variant="contained"
color="accent"
type="submit"
sx={{
width: "100%",
alignSelf: "center",
fontWeight: 700,
mt: theme.spacing(10),
}}
>
{t("auth.common.navigation.continue")}
</Button>
</Stack>
</Stack>
</AuthPageWrapper>
);
};
@@ -9,7 +9,7 @@ import ThemeSwitch from "../../../Components/ThemeSwitch";
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
const AuthHeader = () => {
const AuthHeader = ({ hideLogo = false }) => {
// Hooks
const theme = useTheme();
const { t } = useTranslation();
@@ -27,8 +27,12 @@ const AuthHeader = () => {
alignItems="center"
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>{t("common.appName")}</Typography>
{!hideLogo && (
<>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>{t("common.appName")}</Typography>
</>
)}
</Stack>
<Stack
direction="row"
@@ -0,0 +1,107 @@
import Background from "../../../assets/Images/background-grid.svg?react";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import AuthHeader from "../components/AuthHeader";
import { useTheme } from "@mui/material/styles";
import Logo from "../../../assets/icons/checkmate-icon.svg?react";
import PropTypes from "prop-types";
const AuthPageWrapper = ({ children, heading, welcome }) => {
const theme = useTheme();
return (
<Stack
gap={theme.spacing(10)}
minHeight="100vh"
position="relative"
backgroundColor={theme.palette.primary.main}
sx={{ overflow: "hidden" }}
>
<AuthHeader hideLogo={true} />
<Box
sx={{
position: "absolute",
top: 0,
left: "0%",
transform: "translate(-40%, -40%)",
zIndex: 0,
width: "100%",
height: "100%",
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
<Box
sx={{
position: "absolute",
bottom: 0,
right: 0,
transform: "translate(45%, 55%)",
zIndex: 0,
width: "100%",
height: "100%",
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
<Stack
backgroundColor={theme.palette.primary.main}
sx={{
borderRadius: theme.spacing(8),
boxShadow: theme.palette.tertiary.cardShadow,
margin: "auto",
alignItems: "center",
gap: theme.spacing(10),
padding: theme.spacing(20),
zIndex: 1,
position: "relative",
width: {
sm: "60%",
md: "50%",
lg: "40%",
xl: "30%",
},
}}
>
<Box
mb={theme.spacing(10)}
mt={theme.spacing(5)}
>
<Box
sx={{
width: { xs: 60, sm: 70, md: 80 },
}}
/>
<Logo style={{ width: "100%", height: "100%" }} />
</Box>
<Stack
mb={theme.spacing(4)}
textAlign="center"
>
<Typography
variant="h1"
mb={theme.spacing(2)}
>
{welcome}
</Typography>
<Typography variant="h1">{heading}</Typography>
</Stack>
{children}
</Stack>
</Stack>
);
};
export default AuthPageWrapper;
AuthPageWrapper.propTypes = {
children: PropTypes.node,
heading: PropTypes.node,
welcome: PropTypes.node,
};
@@ -0,0 +1,116 @@
import Check from "../../../Components/Check/Check";
import Stack from "@mui/material/Stack";
import { Tooltip, useTheme } from "@mui/material";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
const PasswordTooltip = ({ feedback, form, children }) => {
const theme = useTheme();
const { t } = useTranslation();
const hasPassword = form.password.length > 0;
const hasInvalidFeedback = Object.values(feedback).some(
(status) => status !== "success"
);
const showPasswordTooltip = hasPassword && hasInvalidFeedback;
return (
<Tooltip
placement="right"
arrow
open={showPasswordTooltip}
title={
<Stack
gap={theme.spacing(4)}
mb={{ xs: theme.spacing(6), sm: theme.spacing(8) }}
>
<Check
noHighlightText={
t("auth.common.inputs.password.rules.length.beginning") +
" " +
t("auth.common.inputs.password.rules.length.highlighted")
}
variant={feedback.length}
/>
<Check
noHighlightText={
t("auth.common.inputs.password.rules.special.beginning") +
" " +
t("auth.common.inputs.password.rules.special.highlighted")
}
variant={feedback.special}
/>
<Check
noHighlightText={
t("auth.common.inputs.password.rules.number.beginning") +
" " +
t("auth.common.inputs.password.rules.number.highlighted")
}
variant={feedback.number}
/>
<Check
noHighlightText={
t("auth.common.inputs.password.rules.uppercase.beginning") +
" " +
t("auth.common.inputs.password.rules.uppercase.highlighted")
}
variant={feedback.uppercase}
/>
<Check
noHighlightText={
t("auth.common.inputs.password.rules.lowercase.beginning") +
" " +
t("auth.common.inputs.password.rules.lowercase.highlighted")
}
variant={feedback.lowercase}
/>
<Check
noHighlightText={
t("auth.common.inputs.password.rules.match.beginning") +
" " +
t("auth.common.inputs.password.rules.match.highlighted")
}
variant={feedback.confirm}
/>
</Stack>
}
slotProps={{
tooltip: {
sx: {
backgroundColor: theme.palette.tertiary.background,
border: `0.5px solid ${theme.palette.primary.lowContrast}90`,
borderRadius: theme.spacing(4),
color: theme.palette.primary.contrastText,
width: "auto",
maxWidth: { xs: "25vw", md: "none" },
whiteSpace: { xs: "normal", md: "nowrap" },
paddingTop: theme.spacing(8),
px: theme.spacing(8),
},
},
arrow: {
sx: {
color: theme.palette.tertiary.background,
},
},
}}
>
<span style={{ display: "inline-block", width: "100%" }}>{children}</span>
</Tooltip>
);
};
PasswordTooltip.propTypes = {
feedback: PropTypes.shape({
length: PropTypes.string,
special: PropTypes.string,
number: PropTypes.string,
uppercase: PropTypes.string,
lowercase: PropTypes.string,
confirm: PropTypes.string,
}),
form: PropTypes.shape({
password: PropTypes.string.isRequired,
}),
children: PropTypes.node,
};
export default PasswordTooltip;
@@ -0,0 +1,71 @@
import { useState } from "react";
import { newOrChangedCredentials } from "../../../Validation/validation";
const usePasswordFeedback = () => {
const [feedback, setFeedback] = useState({});
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 handlePasswordFeedback = (updatedForm, name, value, form, errors, setErrors) => {
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);
};
return { feedback, handlePasswordFeedback, getFeedbackStatus };
};
export default usePasswordFeedback;
@@ -1,5 +1,10 @@
//Components
import Table from "../../../../Components/Table";
import Stack from "@mui/material/Stack";
import DataTable from "../../../../Components/Table";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableRow from "@mui/material/TableRow";
import TableCell from "@mui/material/TableCell";
import TableSkeleton from "../../../../Components/Table/skeleton";
import Pagination from "../../../../Components/Table/TablePagination";
import { StatusLabel } from "../../../../Components/Label";
@@ -16,7 +21,68 @@ import { useTranslation } from "react-i18next";
import { useFetchChecksTeam } from "../../../../Hooks/checkHooks";
import { useFetchChecksByMonitor } from "../../../../Hooks/checkHooks";
import { useResolveIncident } from "../../../../Hooks/checkHooks";
import { Button, Typography } from "@mui/material";
import { Button, Typography, useTheme } from "@mui/material";
import { lighten } from "@mui/material/styles";
const GetTooltip = (row) => {
const theme = useTheme();
const phases = row?.timings?.phases;
const phaseKeyFormattingMap = {
firstByte: "first byte",
};
return (
<Stack
backgroundColor={lighten(theme.palette.primary.main, 0.1)}
border={`1px solid ${theme.palette.primary.lowContrast}`}
borderRadius={theme.shape.borderRadius}
py={theme.spacing(2)}
px={theme.spacing(4)}
>
<Typography
variant="body2"
color={theme.palette.primary.contrastText}
>{`Status code: ${row?.statusCode}`}</Typography>
<Typography
variant="body2"
color={theme.palette.primary.contrastText}
>{`Response time: ${row?.responseTime} ms`}</Typography>
{phases && (
<>
<Typography
variant="body2"
color={theme.palette.primary.contrastText}
>{`Request timing: `}</Typography>
<Table
size="small"
sx={{ ml: theme.spacing(2), mt: theme.spacing(2) }}
>
<TableBody>
{Object.keys(phases)?.map((phaseKey) => (
<TableRow key={phaseKey}>
<TableCell sx={{ border: "none", p: 0 }}>
<Typography
variant="body2"
color="success"
>
{`${phaseKeyFormattingMap[phaseKey] || phaseKey}:`}
</Typography>
</TableCell>
<TableCell sx={{ border: "none", p: 0 }}>
<Typography
color={theme.palette.primary.contrastText}
variant="body2"
>{`${phases[phaseKey]} ms`}</Typography>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</>
)}
</Stack>
);
};
const IncidentTable = ({
isLoading,
@@ -167,9 +233,10 @@ const IncidentTable = ({
return (
<>
<Table
<DataTable
headers={headers}
data={checks}
config={{ tooltipContent: GetTooltip }}
/>
<Pagination
paginationLabel={t("incidentsTablePaginationLabel")}
@@ -2,18 +2,22 @@ import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography } from "@mui/material";
import Background from "../../../../assets/Images/background-grid.svg?react";
import MonitorHeartOutlinedIcon from "@mui/icons-material/MonitorHeartOutlined";
import TaskAltOutlinedIcon from "@mui/icons-material/TaskAltOutlined";
import CancelOutlinedIcon from "@mui/icons-material/CancelOutlined";
import WarningAmberRoundedIcon from "@mui/icons-material/WarningAmberRounded";
import AlertIcon from "../../../../assets/icons/alert-icon.svg?react";
import CheckIcon from "../../../../assets/icons/check-icon.svg?react";
import CloseIcon from "../../../../assets/icons/close-icon.svg?react";
import WarningIcon from "../../../../assets/icons/warning-icon.svg?react";
const StatusBox = ({ title, value, status }) => {
const theme = useTheme();
let sharedStyles = {
position: "absolute",
right: 8,
opacity: 0.5,
"& svg path": { stroke: theme.palette.primary.contrastTextTertiary },
"& svg": {
width: 20,
height: 20,
opacity: 0.9,
"& path": { stroke: theme.palette.primary.contrastTextTertiary, strokeWidth: 1.7 },
},
};
let color;
@@ -22,28 +26,28 @@ const StatusBox = ({ title, value, status }) => {
color = theme.palette.success.lowContrast;
icon = (
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
<TaskAltOutlinedIcon fontSize="small" />
<CheckIcon />
</Box>
);
} else if (status === "down") {
color = theme.palette.error.lowContrast;
icon = (
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
<CancelOutlinedIcon fontSize="small" />
<CloseIcon />
</Box>
);
} else if (status === "paused") {
color = theme.palette.warning.lowContrast;
icon = (
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
<WarningAmberRoundedIcon fontSize="small" />
<WarningIcon />
</Box>
);
} else {
color = theme.palette.accent.main;
icon = (
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
<MonitorHeartOutlinedIcon fontSize="small" />
<AlertIcon />
</Box>
);
}
+10 -7
View File
@@ -11,7 +11,7 @@ import { Box, Button } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useFetchMonitorsByTeamId } from "../../Hooks/monitorHooks";
import { useFetchChecksSummaryByTeamId } from "../../Hooks/checkHooks";
import { useAckAllChecks } from "../../Hooks/checkHooks";
import { useAcknowledgeChecks } from "../../Hooks/checkHooks";
import { useState, useEffect } from "react";
import NetworkError from "../../Components/GenericFallback/NetworkError";
import { useTranslation } from "react-i18next";
@@ -34,7 +34,7 @@ const Incidents = () => {
const [updateTrigger, setUpdateTrigger] = useState(false);
//Hooks
const [ackAllChecks, ackAllLoading] = useAckAllChecks();
const { acknowledge, isLoadingAcknowledge } = useAcknowledgeChecks();
//Utils
const theme = useTheme();
@@ -62,8 +62,9 @@ const Incidents = () => {
setMonitorLookup(monitorLookup);
}, [monitors]);
const handleAckAllChecks = () => {
ackAllChecks(setUpdateTrigger);
const handleAcknowledge = () => {
const monitorId = selectedMonitor === "0" ? null : selectedMonitor;
acknowledge(setUpdateTrigger, monitorId);
};
if (networkError || networkErrorSummary) {
@@ -81,10 +82,12 @@ const Incidents = () => {
<Button
variant="contained"
color="accent"
onClick={handleAckAllChecks}
disabled={ackAllLoading}
onClick={handleAcknowledge}
disabled={isLoadingAcknowledge}
>
{t("incidentsPageActionResolve")}
{selectedMonitor === "0"
? t("incidentsPageActionResolveAll")
: t("incidentsPageActionResolveMonitor")}
</Button>
</Box>
<StatusBoxes
@@ -0,0 +1,87 @@
import ConfigBox from "../../../../Components/ConfigBox";
import { Box, Stack, Typography } from "@mui/material";
import { CustomThreshold } from "../Components/CustomThreshold";
import { capitalizeFirstLetter } from "../../../../Utils/stringUtils";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
const CustomAlertsSection = ({
errors,
onChange,
infrastructureMonitor,
handleCheckboxChange,
}) => {
const theme = useTheme();
const { t } = useTranslation();
const METRICS = ["cpu", "memory", "disk", "temperature"];
const METRIC_PREFIX = "usage_";
const hasAlertError = (errors) => {
return Object.keys(errors).filter((k) => k.startsWith(METRIC_PREFIX)).length > 0;
};
const getAlertError = (errors) => {
const errorKey = Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX));
return errorKey ? errors[errorKey] : null;
};
return (
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("infrastructureCustomizeAlerts")}
</Typography>
<Typography component="p">
{t("infrastructureAlertNotificationDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(6)}>
{METRICS.map((metric) => {
return (
<CustomThreshold
key={metric}
infrastructureMonitor={infrastructureMonitor}
errors={errors}
checkboxId={metric}
checkboxName={metric}
checkboxLabel={
metric !== "cpu" ? capitalizeFirstLetter(metric) : metric.toUpperCase()
}
onCheckboxChange={handleCheckboxChange}
isChecked={infrastructureMonitor[metric]}
fieldId={METRIC_PREFIX + metric}
fieldName={METRIC_PREFIX + metric}
fieldValue={String(infrastructureMonitor[METRIC_PREFIX + metric])}
onFieldChange={onChange}
alertUnit={metric == "temperature" ? "°C" : "%"}
/>
);
})}
{hasAlertError(errors) && (
<Typography
component="span"
className="input-error"
color={theme.palette.error.main}
mt={theme.spacing(2)}
sx={{
opacity: 0.8,
}}
>
{getAlertError(errors)}
</Typography>
)}
</Stack>
</ConfigBox>
);
};
CustomAlertsSection.propTypes = {
errors: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
infrastructureMonitor: PropTypes.object.isRequired,
handleCheckboxChange: PropTypes.func.isRequired,
};
export default CustomAlertsSection;
@@ -62,14 +62,15 @@ export const CustomThreshold = ({
const theme = useTheme();
return (
<Stack
direction={"row"}
sx={{
width: "50%",
justifyContent: "space-between",
flexWrap: "wrap",
}}
direction={{ sm: "column", md: "row" }}
spacing={theme.spacing(2)}
>
<Box>
<Box
sx={{
width: { md: "45%", lg: "25%", xl: "20%" },
}}
justifyContent="flex-start"
>
<Checkbox
id={checkboxId}
name={checkboxName}
@@ -81,8 +82,10 @@ export const CustomThreshold = ({
<Stack
direction={"row"}
sx={{
justifyContent: "flex-end",
justifyContent: "flex-start",
}}
alignItems="center"
spacing={theme.spacing(4)}
>
<TextInput
maxWidth="var(--env-var-width-4)"
@@ -0,0 +1,78 @@
import { useState } from "react";
import { Box, Button } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded";
import Dialog from "../../../../Components/Dialog";
import PropTypes from "prop-types";
const MonitorActionButtons = ({ monitor, isBusy, handlePause, handleRemove }) => {
const theme = useTheme();
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
return (
<Box
alignSelf="flex-end"
ml="auto"
>
<Button
onClick={handlePause}
loading={isBusy}
variant="contained"
color="secondary"
sx={{
pl: theme.spacing(4),
pr: theme.spacing(6),
"& svg": {
mr: theme.spacing(2),
"& path": {
stroke: theme.palette.primary.contrastTextTertiary,
strokeWidth: 0.1,
},
},
}}
>
{monitor?.isActive ? (
<>
<PauseCircleOutlineIcon />
{t("pause")}
</>
) : (
<>
<PlayCircleOutlineRoundedIcon />
{t("resume")}
</>
)}
</Button>
<Button
loading={isBusy}
variant="contained"
color="error"
onClick={() => setIsOpen(true)}
sx={{ ml: theme.spacing(6) }}
>
{t("remove")}
</Button>
<Dialog
open={isOpen}
theme={theme}
title={t("deleteDialogTitle")}
description={t("deleteDialogDescription")}
onCancel={() => setIsOpen(false)}
confirmationButtonLabel={t("delete")}
onConfirm={handleRemove}
/>
</Box>
);
};
MonitorActionButtons.propTypes = {
monitor: PropTypes.object.isRequired,
isBusy: PropTypes.bool.isRequired,
handlePause: PropTypes.func.isRequired,
handleRemove: PropTypes.func.isRequired,
};
export default MonitorActionButtons;
@@ -0,0 +1,73 @@
import { Box, Stack, Tooltip, Typography } from "@mui/material";
import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import PulseDot from "../../../../Components/Animated/PulseDot";
import PropTypes from "prop-types";
const MonitorStatusHeader = ({ monitor, infrastructureMonitor }) => {
const theme = useTheme();
const { t } = useTranslation();
const { statusColor, pagespeedStatusMsg, determineState } = useMonitorUtils();
return (
<Stack
direction="row"
alignItems="center"
height="fit-content"
gap={theme.spacing(2)}
>
<Tooltip
title={pagespeedStatusMsg[determineState(monitor)]}
disableInteractive
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: { offset: [0, -8] },
},
],
},
}}
>
<Box>
<PulseDot color={statusColor[determineState(monitor)]} />
</Box>
</Tooltip>
<Typography
component="h2"
variant="monitorUrl"
>
{infrastructureMonitor.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Typography
position="relative"
variant="body2"
ml={theme.spacing(6)}
mt={theme.spacing(1)}
sx={{
"&:before": {
position: "absolute",
content: `""`,
width: theme.spacing(2),
height: theme.spacing(2),
borderRadius: "50%",
backgroundColor: theme.palette.primary.contrastTextTertiary,
opacity: 0.8,
left: theme.spacing(-5),
top: "50%",
transform: "translateY(-50%)",
},
}}
>
{t("editing")}
</Typography>
</Stack>
);
};
MonitorStatusHeader.propTypes = {
monitor: PropTypes.object.isRequired,
infrastructureMonitor: PropTypes.object.isRequired,
};
export default MonitorStatusHeader;
@@ -0,0 +1,96 @@
import { useState, useCallback } from "react";
const useInfrastructureMonitorForm = () => {
const [infrastructureMonitor, setInfrastructureMonitor] = useState({
url: "",
name: "",
notifications: [],
notify_email: false,
interval: 0.25,
statusWindowSize: 5,
statusWindowThreshold: 60,
cpu: false,
usage_cpu: "",
memory: false,
usage_memory: "",
disk: false,
usage_disk: "",
temperature: false,
usage_temperature: "",
secret: "",
});
const onChangeForm = (name, value) => {
setInfrastructureMonitor({
...infrastructureMonitor,
[name]: value,
});
};
const handleCheckboxChange = (event) => {
setInfrastructureMonitor({
...infrastructureMonitor,
[event.target.name]: event.target.checked,
});
};
const initializeInfrastructureMonitorForCreate = useCallback((globalSettings) => {
const gt = globalSettings?.data?.settings?.globalThresholds || {};
setInfrastructureMonitor((prev) => ({
...prev,
url: "",
name: "",
notifications: [],
interval: 0.25,
cpu: gt.cpu !== undefined,
usage_cpu: gt.cpu !== undefined ? gt.cpu.toString() : "",
memory: gt.memory !== undefined,
usage_memory: gt.memory !== undefined ? gt.memory.toString() : "",
disk: gt.disk !== undefined,
usage_disk: gt.disk !== undefined ? gt.disk.toString() : "",
temperature: gt.temperature !== undefined,
usage_temperature: gt.temperature !== undefined ? gt.temperature.toString() : "",
secret: "",
}));
}, []);
const initializeInfrastructureMonitorForUpdate = useCallback((monitor) => {
const MS_PER_MINUTE = 60000;
const { thresholds = {} } = monitor;
setInfrastructureMonitor((prev) => ({
...prev,
url: monitor.url.replace(/^https?:\/\//, ""),
name: monitor.name || "",
notifications: monitor.notifications || [],
interval: monitor.interval / MS_PER_MINUTE,
statusWindowSize: monitor.statusWindowSize,
statusWindowThreshold: monitor.statusWindowThreshold,
cpu: thresholds.usage_cpu !== undefined,
usage_cpu:
thresholds.usage_cpu !== undefined ? (thresholds.usage_cpu * 100).toString() : "",
memory: thresholds.usage_memory !== undefined,
usage_memory:
thresholds.usage_memory !== undefined
? (thresholds.usage_memory * 100).toString()
: "",
disk: thresholds.usage_disk !== undefined,
usage_disk:
thresholds.usage_disk !== undefined
? (thresholds.usage_disk * 100).toString()
: "",
temperature: thresholds.usage_temperature !== undefined,
usage_temperature:
thresholds.usage_temperature !== undefined
? (thresholds.usage_temperature * 100).toString()
: "",
secret: monitor.secret || "",
}));
}, []);
return {
infrastructureMonitor,
setInfrastructureMonitor,
onChangeForm,
handleCheckboxChange,
initializeInfrastructureMonitorForCreate,
initializeInfrastructureMonitorForUpdate,
};
};
export default useInfrastructureMonitorForm;
@@ -0,0 +1,82 @@
import { useCreateMonitor, useUpdateMonitor } from "../../../../Hooks/monitorHooks";
const useInfrastructureSubmit = () => {
const [createMonitor, isCreating] = useCreateMonitor();
const [updateMonitor, isUpdating] = useUpdateMonitor();
const buildForm = (infrastructureMonitor, https) => {
const MS_PER_MINUTE = 60000;
let form = {
url: `http${https ? "s" : ""}://` + infrastructureMonitor.url,
name:
infrastructureMonitor.name === ""
? infrastructureMonitor.url
: infrastructureMonitor.name,
interval: infrastructureMonitor.interval * MS_PER_MINUTE,
statusWindowSize: infrastructureMonitor.statusWindowSize,
statusWindowThreshold: infrastructureMonitor.statusWindowThreshold,
cpu: infrastructureMonitor.cpu,
...(infrastructureMonitor.cpu
? { usage_cpu: infrastructureMonitor.usage_cpu }
: {}),
memory: infrastructureMonitor.memory,
...(infrastructureMonitor.memory
? { usage_memory: infrastructureMonitor.usage_memory }
: {}),
disk: infrastructureMonitor.disk,
...(infrastructureMonitor.disk
? { usage_disk: infrastructureMonitor.usage_disk }
: {}),
temperature: infrastructureMonitor.temperature,
...(infrastructureMonitor.temperature
? { usage_temperature: infrastructureMonitor.usage_temperature }
: {}),
secret: infrastructureMonitor.secret,
};
return form;
};
const submitInfrastructureForm = async (
infrastructureMonitor,
form,
isCreate,
monitorId
) => {
const {
cpu,
usage_cpu,
memory,
usage_memory,
disk,
usage_disk,
temperature,
usage_temperature,
...rest
} = form;
const thresholds = {
...(cpu ? { usage_cpu: usage_cpu / 100 } : {}),
...(memory ? { usage_memory: usage_memory / 100 } : {}),
...(disk ? { usage_disk: usage_disk / 100 } : {}),
...(temperature ? { usage_temperature: usage_temperature / 100 } : {}),
};
const finalForm = {
...(isCreate ? {} : { _id: monitorId }),
...rest,
description: form.name,
type: "hardware",
notifications: infrastructureMonitor.notifications,
thresholds,
};
// Handle create or update
isCreate
? await createMonitor({ monitor: finalForm, redirect: "/infrastructure" })
: await updateMonitor({ monitor: finalForm, redirect: "/infrastructure" });
};
return {
buildForm,
submitInfrastructureForm,
isCreating,
isUpdating,
};
};
export default useInfrastructureSubmit;
@@ -0,0 +1,37 @@
import { useState } from "react";
import { infrastructureMonitorValidation } from "../../../../Validation/validation";
import { createToast } from "../../../../Utils/toastUtils";
const useValidateInfrastructureForm = () => {
const [errors, setErrors] = useState({});
const validateField = (name, value) => {
const { error } = infrastructureMonitorValidation.validate(
{ [name]: value },
{ abortEarly: false }
);
setErrors((prev) => ({
...prev,
...(error ? { [name]: error.details[0].message } : { [name]: undefined }),
}));
};
const validateForm = (form) => {
const { error } = infrastructureMonitorValidation.validate(form, {
abortEarly: false,
});
if (error) {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
console.log(newErrors);
setErrors(newErrors);
createToast({ body: "Please check the form for errors." });
return error;
}
return null;
};
return { errors, validateField, validateForm };
};
export default useValidateInfrastructureForm;
+193 -271
View File
@@ -1,241 +1,141 @@
// React, Redux, Router
import { useTheme } from "@emotion/react";
import { useParams } from "react-router-dom";
import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
// Utility and Network
import { infrastructureMonitorValidation } from "../../../Validation/validation";
import { useFetchHardwareMonitorById } from "../../../Hooks/monitorHooks";
import { capitalizeFirstLetter } from "../../../Utils/stringUtils";
import { useTranslation } from "react-i18next";
import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications";
import NotificationsConfig from "../../../Components/NotificationConfig";
import { useUpdateMonitor, useCreateMonitor } from "../../../Hooks/monitorHooks";
// MUI
import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
//Components
import Breadcrumbs from "../../../Components/Breadcrumbs";
import Link from "../../../Components/Link";
import ConfigBox from "../../../Components/ConfigBox";
import TextInput from "../../../Components/Inputs/TextInput";
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
import { createToast } from "../../../Utils/toastUtils";
import FieldWrapper from "../../../Components/Inputs/FieldWrapper";
import Link from "../../../Components/Link";
import Select from "../../../Components/Inputs/Select";
import { CustomThreshold } from "./Components/CustomThreshold";
const SELECT_VALUES = [
{ _id: 0.25, name: "15 seconds" },
{ _id: 0.5, name: "30 seconds" },
{ _id: 1, name: "1 minute" },
{ _id: 2, name: "2 minutes" },
{ _id: 5, name: "5 minutes" },
{ _id: 10, name: "10 minutes" },
];
const METRICS = ["cpu", "memory", "disk", "temperature"];
const METRIC_PREFIX = "usage_";
const MS_PER_MINUTE = 60000;
const hasAlertError = (errors) => {
return Object.keys(errors).filter((k) => k.startsWith(METRIC_PREFIX)).length > 0;
};
const getAlertError = (errors) => {
return Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX))
? errors[Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX))]
: null;
};
import TextInput from "../../../Components/Inputs/TextInput";
import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
import MonitorStatusHeader from "./Components/MonitorStatusHeader";
import MonitorActionButtons from "./Components/MonitorActionButtons";
import CustomAlertsSection from "./Components/CustomAlertsSection";
// Utils
import NotificationsConfig from "../../../Components/NotificationConfig";
import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications";
import { useParams } from "react-router-dom";
import { useState, useEffect } from "react";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import {
useDeleteMonitor,
useFetchGlobalSettings,
useFetchHardwareMonitorById,
usePauseMonitor,
} from "../../../Hooks/monitorHooks";
import useInfrastructureMonitorForm from "./hooks/useInfrastructureMonitorForm";
import useValidateInfrastructureForm from "./hooks/useValidateInfrastructureForm";
import useInfrastructureSubmit from "./hooks/useInfrastructureSubmit";
const CreateInfrastructureMonitor = () => {
const theme = useTheme();
const { user } = useSelector((state) => state.auth);
const { monitorId } = useParams();
const { t } = useTranslation();
// Determine if we are creating or editing
const isCreate = typeof monitorId === "undefined";
// Fetch monitor details if editing
const [monitor, isLoading, networkError] = useFetchHardwareMonitorById({ monitorId });
const [notifications, notificationsAreLoading, notificationsError] =
useGetNotificationsByTeamId();
const [updateMonitor, isUpdating] = useUpdateMonitor();
const [createMonitor, isCreating] = useCreateMonitor();
const theme = useTheme();
const { t } = useTranslation();
// State
const [errors, setErrors] = useState({});
const [https, setHttps] = useState(false);
const [infrastructureMonitor, setInfrastructureMonitor] = useState({
url: "",
name: "",
notifications: [],
notify_email: false,
interval: 0.25,
cpu: false,
usage_cpu: "",
memory: false,
usage_memory: "",
disk: false,
usage_disk: "",
temperature: false,
usage_temperature: "",
secret: "",
});
const [updateTrigger, setUpdateTrigger] = useState(false);
// Fetch monitor details if editing
const [monitor, isLoading] = useFetchHardwareMonitorById({
monitorId,
updateTrigger,
});
const [deleteMonitor, isDeleting] = useDeleteMonitor();
const [globalSettings, globalSettingsLoading] = useFetchGlobalSettings();
const [notifications, notificationsAreLoading] = useGetNotificationsByTeamId();
const [pauseMonitor, isPausing] = usePauseMonitor();
const {
infrastructureMonitor,
setInfrastructureMonitor,
onChangeForm,
handleCheckboxChange,
initializeInfrastructureMonitorForCreate,
initializeInfrastructureMonitorForUpdate,
} = useInfrastructureMonitorForm();
const { errors, validateField, validateForm } = useValidateInfrastructureForm();
const { buildForm, submitInfrastructureForm, isCreating, isUpdating } =
useInfrastructureSubmit();
const FREQUENCIES = [
{ _id: 0.25, name: t("time.fifteenSeconds") },
{ _id: 0.5, name: t("time.thirtySeconds") },
{ _id: 1, name: t("time.oneMinute") },
{ _id: 2, name: t("time.twoMinutes") },
{ _id: 5, name: t("time.fiveMinutes") },
{ _id: 10, name: t("time.tenMinutes") },
];
const CRUMBS = [
{ name: "Infrastructure monitors", path: "/infrastructure" },
...(isCreate
? [{ name: "Create", path: "/infrastructure/create" }]
: [
{ name: "Details", path: `/infrastructure/${monitorId}` },
{ name: "Configure", path: `/infrastructure/configure/${monitorId}` },
]),
];
// Populate form fields if editing
useEffect(() => {
if (isCreate || !monitor) return;
setInfrastructureMonitor({
url: monitor.url.replace(/^https?:\/\//, ""),
name: monitor.name || "",
notifications: monitor.notifications,
interval: monitor.interval / MS_PER_MINUTE,
cpu: monitor.thresholds?.usage_cpu !== undefined,
usage_cpu: monitor.thresholds?.usage_cpu ? monitor.thresholds.usage_cpu * 100 : "",
memory: monitor.thresholds?.usage_memory !== undefined,
usage_memory: monitor.thresholds?.usage_memory
? monitor.thresholds.usage_memory * 100
: "",
disk: monitor.thresholds?.usage_disk !== undefined,
usage_disk: monitor.thresholds?.usage_disk
? monitor.thresholds.usage_disk * 100
: "",
temperature: monitor.thresholds?.usage_temperature !== undefined,
usage_temperature: monitor.thresholds?.usage_temperature
? monitor.thresholds.usage_temperature * 100
: "",
secret: monitor.secret || "",
});
setHttps(monitor.url.startsWith("https"));
}, [isCreate, monitor]);
if (isCreate) {
if (globalSettingsLoading) return;
setHttps(false);
initializeInfrastructureMonitorForCreate(globalSettings);
} else if (monitor) {
setHttps(monitor.url.startsWith("https"));
initializeInfrastructureMonitorForUpdate(monitor);
}
}, [
isCreate,
monitor,
globalSettings,
globalSettingsLoading,
initializeInfrastructureMonitorForCreate,
initializeInfrastructureMonitorForUpdate,
]);
// Handlers
const onSubmit = async (event) => {
event.preventDefault();
// Build the form
let form = {
url: `http${https ? "s" : ""}://` + infrastructureMonitor.url,
name:
infrastructureMonitor.name === ""
? infrastructureMonitor.url
: infrastructureMonitor.name,
interval: infrastructureMonitor.interval * MS_PER_MINUTE,
cpu: infrastructureMonitor.cpu,
...(infrastructureMonitor.cpu
? { usage_cpu: infrastructureMonitor.usage_cpu }
: {}),
memory: infrastructureMonitor.memory,
...(infrastructureMonitor.memory
? { usage_memory: infrastructureMonitor.usage_memory }
: {}),
disk: infrastructureMonitor.disk,
...(infrastructureMonitor.disk
? { usage_disk: infrastructureMonitor.usage_disk }
: {}),
temperature: infrastructureMonitor.temperature,
...(infrastructureMonitor.temperature
? { usage_temperature: infrastructureMonitor.usage_temperature }
: {}),
secret: infrastructureMonitor.secret,
};
const { error } = infrastructureMonitorValidation.validate(form, {
abortEarly: false,
});
const form = buildForm(infrastructureMonitor, https);
const error = validateForm(form);
if (error) {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
console.log(newErrors);
setErrors(newErrors);
createToast({ body: "Please check the form for errors." });
return;
}
submitInfrastructureForm(infrastructureMonitor, form, isCreate, monitorId);
};
// Build the thresholds for the form
const {
cpu,
usage_cpu,
memory,
usage_memory,
disk,
usage_disk,
temperature,
usage_temperature,
...rest
} = form;
const thresholds = {
...(cpu ? { usage_cpu: usage_cpu / 100 } : {}),
...(memory ? { usage_memory: usage_memory / 100 } : {}),
...(disk ? { usage_disk: usage_disk / 100 } : {}),
...(temperature ? { usage_temperature: usage_temperature / 100 } : {}),
};
form = {
...(isCreate ? {} : { _id: monitorId }),
...rest,
description: form.name,
type: "hardware",
notifications: infrastructureMonitor.notifications,
thresholds,
};
// Handle create or update
isCreate
? await createMonitor({ monitor: form, redirect: "/infrastructure" })
: await updateMonitor({ monitor: form, redirect: "/infrastructure" });
const triggerUpdate = () => {
setUpdateTrigger(!updateTrigger);
};
const onChange = (event) => {
const { value, name } = event.target;
setInfrastructureMonitor({
...infrastructureMonitor,
[name]: value,
});
const { error } = infrastructureMonitorValidation.validate(
{ [name]: value },
{ abortEarly: false }
);
setErrors((prev) => ({
...prev,
...(error ? { [name]: error.details[0].message } : { [name]: undefined }),
}));
onChangeForm(name, value);
validateField(name, value);
};
const handleCheckboxChange = (event) => {
const { name } = event.target;
const { checked } = event.target;
setInfrastructureMonitor({
...infrastructureMonitor,
[name]: checked,
});
const handlePause = async () => {
await pauseMonitor({ monitorId, triggerUpdate });
};
const handleRemove = async (event) => {
event.preventDefault();
await deleteMonitor({ monitor, redirect: "/infrastructure" });
};
const isBusy =
isLoading ||
isUpdating ||
isCreating ||
isDeleting ||
isPausing ||
notificationsAreLoading;
return (
<Box className="create-infrastructure-monitor">
<Breadcrumbs
list={[
{ name: "Infrastructure monitors", path: "/infrastructure" },
...(isCreate
? [{ name: "Create", path: "/infrastructure/create" }]
: [
{ name: "Details", path: `/infrastructure/${monitorId}` },
{ name: "Configure", path: `/infrastructure/configure/${monitorId}` },
]),
]}
/>
<Breadcrumbs list={CRUMBS} />
<Stack
component="form"
onSubmit={onSubmit}
@@ -244,27 +144,55 @@ const CreateInfrastructureMonitor = () => {
gap={theme.spacing(12)}
mt={theme.spacing(6)}
>
<Typography
component="h1"
variant="h1"
<Stack
direction="row"
gap={theme.spacing(2)}
>
<Typography
component="span"
fontSize="inherit"
>
{t(isCreate ? "infrastructureCreateYour" : "infrastructureEditYour")}{" "}
</Typography>
<Typography
component="span"
variant="h2"
fontSize="inherit"
fontWeight="inherit"
>
{t("monitor")}
</Typography>
</Typography>
<Box>
<Typography
component="h1"
variant="h1"
>
<Typography
component="span"
fontSize="inherit"
color={
!isCreate ? theme.palette.primary.contrastTextSecondary : undefined
}
>
{!isCreate ? infrastructureMonitor.name : t("createYour") + " "}
</Typography>
{isCreate ? (
<Typography
component="span"
fontSize="inherit"
fontWeight="inherit"
color={theme.palette.primary.contrastTextSecondary}
>
{t("monitor")}
</Typography>
) : (
<></>
)}
</Typography>
{!isCreate && (
<MonitorStatusHeader
monitor={monitor}
infrastructureMonitor={infrastructureMonitor}
/>
)}
</Box>
{!isCreate && (
<MonitorActionButtons
monitor={monitor}
isBusy={isBusy}
handlePause={handlePause}
handleRemove={handleRemove}
/>
)}
</Stack>
<ConfigBox>
<Stack gap={theme.spacing(6)}>
<Stack>
<Typography
component="h2"
variant="h2"
@@ -283,7 +211,7 @@ const CreateInfrastructureMonitor = () => {
/>
</Typography>
</Stack>
<Stack gap={theme.spacing(15)}>
<Stack gap={theme.spacing(8)}>
<TextInput
type="url"
id="url"
@@ -299,8 +227,10 @@ const CreateInfrastructureMonitor = () => {
disabled={!isCreate}
/>
{isCreate && (
<Box>
<Typography component="p">{t("infrastructureProtocol")}</Typography>
<FieldWrapper
label={t("infrastructureProtocol")}
labelVariant="p"
>
<ButtonGroup>
<Button
variant="group"
@@ -317,7 +247,7 @@ const CreateInfrastructureMonitor = () => {
{t("http")}
</Button>
</ButtonGroup>
</Box>
</FieldWrapper>
)}
<TextInput
type="text"
@@ -344,7 +274,12 @@ const CreateInfrastructureMonitor = () => {
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">{t("notificationConfig.title")}</Typography>
<Typography
component="h2"
variant="h2"
>
{t("notificationConfig.title")}
</Typography>
<Typography component="p">{t("notificationConfig.description")}</Typography>
</Box>
<NotificationsConfig
@@ -359,52 +294,39 @@ const CreateInfrastructureMonitor = () => {
component="h2"
variant="h2"
>
{t("infrastructureCustomizeAlerts")}
{t("createMonitorPage.incidentConfigTitle")}
</Typography>
<Typography component="p">
{t("infrastructureAlertNotificationDescription")}
{t("createMonitorPage.incidentConfigDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(6)}>
{METRICS.map((metric) => {
return (
<CustomThreshold
key={metric}
infrastructureMonitor={infrastructureMonitor}
errors={errors}
checkboxId={metric}
checkboxName={metric}
checkboxLabel={
metric !== "cpu"
? capitalizeFirstLetter(metric)
: metric.toUpperCase()
}
onCheckboxChange={handleCheckboxChange}
isChecked={infrastructureMonitor[metric]}
fieldId={METRIC_PREFIX + metric}
fieldName={METRIC_PREFIX + metric}
fieldValue={String(infrastructureMonitor[METRIC_PREFIX + metric])}
onFieldChange={onChange}
alertUnit={metric == "temperature" ? "°C" : "%"}
/>
);
})}
{/* Error text */}
{hasAlertError(errors) && (
<Typography
component="span"
className="input-error"
color={theme.palette.error.main}
mt={theme.spacing(2)}
sx={{
opacity: 0.8,
}}
>
{getAlertError(errors)}
</Typography>
)}
<Stack gap={theme.spacing(20)}>
<TextInput
name="statusWindowSize"
label={t("createMonitorPage.incidentConfigStatusWindowLabel")}
type="number"
value={infrastructureMonitor.statusWindowSize}
onChange={onChange}
error={errors["statusWindowSize"] ? true : false}
helperText={errors["statusWindowSize"]}
/>
<TextInput
name="statusWindowThreshold"
label={t("createMonitorPage.incidentConfigStatusWindowThresholdLabel")}
type="number"
value={infrastructureMonitor.statusWindowThreshold}
onChange={onChange}
error={errors["statusWindowThreshold"] ? true : false}
helperText={errors["statusWindowThreshold"]}
/>
</Stack>
</ConfigBox>
<CustomAlertsSection
errors={errors}
onChange={onChange}
infrastructureMonitor={infrastructureMonitor}
handleCheckboxChange={handleCheckboxChange}
/>
<ConfigBox>
<Box>
<Typography
@@ -421,7 +343,7 @@ const CreateInfrastructureMonitor = () => {
label="Check frequency"
value={infrastructureMonitor.interval || 15}
onChange={onChange}
items={SELECT_VALUES}
items={FREQUENCIES}
/>
</Stack>
</ConfigBox>
@@ -433,7 +355,7 @@ const CreateInfrastructureMonitor = () => {
type="submit"
variant="contained"
color="accent"
loading={isLoading || isUpdating || isCreating || notificationsAreLoading}
loading={isBusy}
>
{t(isCreate ? "infrastructureCreateMonitor" : "infrastructureEditMonitor")}
</Button>
@@ -2,18 +2,19 @@
import { Stack } from "@mui/material";
import Gauge from "./Gauge";
import SkeletonLayout from "./skeleton";
import PropTypes from "prop-types";
// Utils
import { useHardwareUtils } from "../../Hooks/useHardwareUtils";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
const Gauges = ({ shouldRender, monitor }) => {
const Gauges = ({ isLoading = false, monitor }) => {
const { decimalToPercentage, formatBytes } = useHardwareUtils();
const theme = useTheme();
const { t } = useTranslation();
if (!shouldRender) {
if (isLoading) {
return <SkeletonLayout />;
}
@@ -60,6 +61,7 @@ const Gauges = ({ shouldRender, monitor }) => {
return (
<Stack
direction="row"
flexWrap="wrap"
gap={theme.spacing(8)}
>
{gauges.map((gauge) => {
@@ -79,4 +81,9 @@ const Gauges = ({ shouldRender, monitor }) => {
);
};
Gauges.propTypes = {
isLoading: PropTypes.bool,
monitor: PropTypes.object,
};
export default Gauges;
Loaded 100 of 400 files, more files were not shown because too many files have changed in this diff. Show more