Compare commits

...
176 Commits
Author SHA1 Message Date
Gorkem Cetin (BWL) c008d2e046 Merge pull request #2362 from mertssmnoglu/docs-readme-mertssmnoglu
fix: correct link format for contributor 'mertssmnoglu' in README
2025-05-28 09:42:30 -04:00
Mert Şişmanoğlu 7cfdad3563 fix: correct link format for contributor mertssmnoglu in README
Signed-off-by: Mert Şişmanoğlu <mertssmnoglu@gmail.com>
2025-05-28 16:33:30 +03:00
Alexander Holliday 5b86d892c5 Merge pull request #2359 from bluewave-labs/master
Merge pull request #2342 from bluewave-labs/develop

develop -> Master
2025-05-27 15:03:18 -07:00
Alexander Holliday 459906ef25 Merge pull request #2358 from bluewave-labs/fix/languages
fix:revert gb to en, add language specific flag fixes
2025-05-27 10:09:33 -07:00
Alex Holliday 1e064fb771 remove style overrides, update script 2025-05-27 10:09:12 -07:00
Alex Holliday 5254dc1984 revert gb to en, add language specific flag fixes 2025-05-27 10:01:10 -07:00
Alexander Holliday f245e49b20 Merge pull request #2357 from bluewave-labs/fix/remove-incorrect-lang-files
remove files
2025-05-27 09:50:05 -07:00
Alex Holliday f25e54eefe remove files 2025-05-27 09:49:15 -07:00
Alexander Holliday bfa713fd7e Merge pull request #2356 from bluewave-labs/translation-update-69
🌐 Updated Translations from POEditor
2025-05-27 09:48:37 -07:00
GitHub Action 73cad941b0 feat: translations updated from POEditor 2025-05-27 16:48:15 +00:00
Alexander Holliday 7d0668279e Merge pull request #2355 from bluewave-labs/hotfix/language-download
hotfix: normalize language codes
2025-05-27 09:47:14 -07:00
Alex Holliday 9b2a9f5731 normalize language codes 2025-05-27 09:46:07 -07:00
Alexander Holliday 9d130ee935 Merge pull request #2353 from bluewave-labs/hotfix/validation
remove showURL validation
2025-05-27 09:26:37 -07:00
Alex Holliday 2758e5f050 remove showURL validation 2025-05-27 09:26:15 -07:00
Alexander Holliday 426545a429 Merge pull request #2352 from bluewave-labs/fix/remove-docker-compose-version
fix: remove version
2025-05-27 09:24:55 -07:00
Alexander Holliday 75f85cc4c8 Merge pull request #2351 from bluewave-labs/translation-update-68
🌐 Updated Translations from POEditor
2025-05-27 09:24:19 -07:00
Alex Holliday 530bea6f45 remove version 2025-05-27 09:21:37 -07:00
GitHub Action a12d733e02 feat: translations updated from POEditor 2025-05-27 16:19:29 +00:00
Alexander Holliday 2e332f791b Merge pull request #2350 from bluewave-labs/feat/format-on-poe-request
update default languages
2025-05-27 09:18:36 -07:00
Alex Holliday deedc9eabd update default languages 2025-05-27 09:18:05 -07:00
Alexander Holliday 5c5e563d42 Merge pull request #2349 from bluewave-labs/feat/format-on-poe-request
feat: add formatting to POEditor github action
2025-05-27 09:15:27 -07:00
Alex Holliday a777777ac6 add formatting to POEditor github action 2025-05-27 09:14:40 -07:00
Alexander Holliday 0dee7cf84f Merge pull request #2348 from bluewave-labs/fix/prettier-format
fix: format all files
2025-05-27 09:11:32 -07:00
Alex Holliday 1b81251c93 format all files 2025-05-27 09:10:32 -07:00
Gorkem Cetin (BWL) 94c2cd0582 Update README.md 2025-05-26 23:52:15 -04:00
Gorkem Cetin (BWL) c1af1cf2cd Update README.md 2025-05-26 23:51:14 -04:00
Alexander Holliday ab964416ae Merge pull request #2342 from bluewave-labs/develop
develop -> Master
2025-05-26 16:42:54 -07:00
Alexander Holliday bfa6197ee4 Merge pull request #2340 from bluewave-labs/hotfix/validation
remove showURL from validation
2025-05-26 16:37:33 -07:00
Alex Holliday 3586fc4e6d remove showURL from validation 2025-05-26 16:35:41 -07:00
Alexander Holliday 5ddb74e70c Merge pull request #2339 from bluewave-labs/revert-2338-hotfix/disable-url-validation
Revert "hotfix: remove showurl validation"
2025-05-26 16:30:21 -07:00
Alexander Holliday 2d52ebad40 Revert "hotfix: remove showurl validation" 2025-05-26 16:30:09 -07:00
Alexander Holliday 02ae6f3bd4 Merge pull request #2338 from bluewave-labs/hotfix/disable-url-validation
hotfix: remove showurl validation
2025-05-26 16:24:51 -07:00
Alex Holliday 7ff8299e6b remove showurl validation 2025-05-26 16:22:54 -07:00
Alexander Holliday ad30bf6ee8 Merge pull request #2335 from sonvir249/feature/show-port-monitors
Show port monitors with port numbers.
2025-05-26 08:44:45 -07:00
sonvir249 8ec6c23250 Join port number with colon(:) 2025-05-26 21:13:05 +05:30
Alexander Holliday a18a62f80e Merge pull request #2327 from AsimAhmedSiddiquii/fix-show-ip-url-status-page
Fix: Optionally show IP/URL of the monitor in status page
2025-05-26 08:32:47 -07:00
Alexander Holliday a0ba1b63c3 Merge pull request #2330 from bluewave-labs/feature/2299-add-ping-monitoring-to-status-pages
Add ping monitoring to status pages (#2299)
2025-05-26 08:12:16 -07:00
Asim Ahmed Siddiqui e7ed85405b Changes: Made showURL boolean and removed console log 2025-05-26 00:19:49 +05:30
sonvir249 118ad67369 Show port monitors with port numbers. 2025-05-25 15:54:31 +05:30
mohadeseh safari d28e2bf5fb Add ping monitoring to status pages (#2299) 2025-05-23 22:45:32 -04:00
Alexander Holliday 9681c7e2ee Merge pull request #2329 from bluewave-labs/fix/test-email
fix: test email
2025-05-23 12:24:05 -07:00
Alex Holliday fa7624e436 proper invocation 2025-05-23 12:22:32 -07:00
Alex Holliday 4aaa1b6674 await config 2025-05-23 12:22:05 -07:00
Asim Ahmed Siddiqui e8d636dcfd Fix: Optionally show IP/URL of the monitor in status page 2025-05-23 12:40:47 +05:30
Alexander Holliday 124683f8c1 Merge pull request #2325 from bluewave-labs/feat/arm-support
update helmet policy
2025-05-22 11:20:48 -07:00
Alex Holliday dd93b9f36b update helmet policy 2025-05-22 11:20:11 -07:00
Alexander Holliday b16634ade9 Merge pull request #2324 from bluewave-labs/feat/arm-support
hsts:false
2025-05-22 10:54:18 -07:00
Alex Holliday ba703ac377 hsts:false 2025-05-22 10:53:44 -07:00
Alexander Holliday 37c0dbf567 Merge pull request #2323 from bluewave-labs/feat/arm-support
Feat/arm support
2025-05-22 10:38:27 -07:00
Alex Holliday 47705f85ef update gitignore, update vite config 2025-05-22 10:37:26 -07:00
Alex Holliday b78389f113 use init sidecar 2025-05-22 10:02:49 -07:00
Alexander Holliday 26fa94abfb Merge pull request #2322 from bluewave-labs/feat/arm-support
init script isntead of healthcheck
2025-05-22 09:55:37 -07:00
Alex Holliday 6d39301ca8 init script isntead of healthcheck 2025-05-22 09:55:07 -07:00
Alexander Holliday 429d68859c Merge pull request #2321 from bluewave-labs/feat/arm-support
feat: arm dist
2025-05-22 09:21:19 -07:00
Alex Holliday d3b446685f arm dist 2025-05-22 09:20:41 -07:00
Alexander Holliday 03bb550003 Merge pull request #2317 from bluewave-labs/Pause-button-addition-on-configuration
Added pause button in configuration page.
2025-05-21 11:41:20 -07:00
Owaise Imdad 97a73a385a Added the status color to monitor utils. 2025-05-21 23:59:22 +05:30
Owaise Imdad 3ddea2ef8b Localised the status msg as well. 2025-05-21 23:32:07 +05:30
Owaise Imdad 9330f2fb0d Console log removal. 2025-05-21 23:26:21 +05:30
Owaise Imdad 35157d87fe Localise the string pause and resume. 2025-05-21 23:25:32 +05:30
Owaise Imdad 36ae38391b Have used the already created callback. 2025-05-21 23:23:09 +05:30
Owaise Imdad deec5a096e Made the code more readable by separation of tasks. 2025-05-21 22:55:34 +05:30
Alexander Holliday 7b117d938f Merge pull request #2294 from bluewave-labs/feature/email-testing-without-saving
Feature/email testing without saving
2025-05-20 13:27:01 -07:00
Alexander Holliday e04212f9e9 Merge branch 'develop' into feature/email-testing-without-saving 2025-05-20 13:26:53 -07:00
Alexander Holliday 62e259e3e6 Merge pull request #2309 from bluewave-labs/feat/jobqueue-error-handling
feat: jobqueue error handling
2025-05-20 13:23:50 -07:00
Owaise Imdad dab2bb466f Added pause button in configuration page. 2025-05-20 21:36:31 +05:30
Alexander Holliday 0628fb2e41 Merge pull request #2314 from ajhollid/fix/env-vars
update config
2025-05-19 17:00:54 -07:00
Alex Holliday fefe124b87 update config 2025-05-19 17:00:28 -07:00
mohadeseh safari d5d3e14d96 Change API and Refactoring a handler function 2025-05-19 19:47:11 -04:00
Alexander Holliday 785b20d903 Merge pull request #2313 from bluewave-labs/fix/action-name
Fix/action name
2025-05-19 15:17:39 -07:00
Alex Holliday 016cd6fef0 update gitignore 2025-05-19 15:17:05 -07:00
Alex Holliday ff819b3f9f Add missing script 2025-05-19 15:16:55 -07:00
Alexander Holliday 667a388fe1 Merge pull request #2312 from bluewave-labs/fix/action-name
fix aciton name
2025-05-19 15:13:55 -07:00
Alex Holliday d9d25bb108 fix aciton name 2025-05-19 15:13:29 -07:00
Alexander Holliday 18eba3dbee Merge pull request #2311 from bluewave-labs/fix/monolithic-docker
fix: monolithic docker
2025-05-19 15:12:37 -07:00
Alex Holliday b6699c8aef user ghcr images 2025-05-19 15:11:02 -07:00
Alex Holliday d6681885e7 update workflows 2025-05-19 15:10:45 -07:00
Alex Holliday 870e84b539 add env vars 2025-05-19 15:04:58 -07:00
Alex Holliday c13a3e93b2 update gitignore 2025-05-19 15:00:57 -07:00
Alex Holliday 72cd3b592e add a docker config for a monolithic app 2025-05-19 15:00:11 -07:00
Alex Holliday 4d5c942464 remove extra logging 2025-05-19 12:29:54 -07:00
Alex Holliday 20911697cc remove double logging 2025-05-19 12:28:18 -07:00
Alex Holliday 08e1e22dad change failed job log level to error 2025-05-19 12:26:25 -07:00
Alexander Holliday 885c63b147 Merge pull request #2291 from bluewave-labs/feat/job-queue-refactor
feat: job queue refactor
2025-05-19 11:28:51 -07:00
Alex Holliday 7651596617 early return in event of bad network response 2025-05-19 11:23:40 -07:00
Alex Holliday fd1edf9651 user factory method 2025-05-19 11:23:26 -07:00
Alex Holliday 9414b87e27 update log msg 2025-05-19 11:23:13 -07:00
Alex Holliday 942104aea2 use logger 2025-05-19 11:22:53 -07:00
Alex Holliday 3ff0f6d399 add factory method, better logging 2025-05-19 11:22:46 -07:00
Alexander Holliday d07063b21b Merge pull request #2308 from bluewave-labs/Incident-button-on-details-page
Added incidents page in the details header.
2025-05-19 11:18:23 -07:00
Owaise Imdad 0be0c5bf3f Removed stop propogation. 2025-05-19 23:47:44 +05:30
Owaise Imdad f82665006c Added incidents page in the details header. 2025-05-19 23:42:33 +05:30
Alexander Holliday 27e02c53b8 Merge pull request #2295 from bluewave-labs/test-email-implementation-in-details-page
Email test button added in details page.
2025-05-19 10:57:40 -07:00
Owaise Imdad 17dcdfcb06 Added all the strings in locale file for gb. 2025-05-19 23:25:36 +05:30
Owaise Imdad 6ba52156b8 Made changes as requested in PR. 2025-05-19 23:21:00 +05:30
Alexander Holliday dbb6e4d37f Merge pull request #2298 from remememe/develop
fix: status page feature checkboxes not saving (Fixes #2274)
2025-05-19 10:07:36 -07:00
Alexander Holliday 586c9d054c Merge pull request #2293 from bluewave-labs/feat/settings-test-email-endpoint
feat: settings test email endpoint
2025-05-19 09:51:58 -07:00
Alexander Holliday 8643ac5661 Merge pull request #2296 from bluewave-labs/dependabot/npm_and_yarn/client/vite-5.4.19
Bump vite from 5.4.18 to 5.4.19 in /client
2025-05-19 09:50:51 -07:00
Alexander Holliday 79806322b1 Merge pull request #2302 from bluewave-labs/consistent-font-size
Consistent font sizes across all create/configure pages
2025-05-19 09:50:30 -07:00
Vishnu Sreekumaran Nair 13e80edb03 update bulk import page font size 2025-05-18 20:37:35 -04:00
Vishnu Sreekumaran Nair 4b22bcebdd update settings page font size 2025-05-18 20:35:55 -04:00
Vishnu Sreekumaran Nair b907ae9705 update status page create/config pages font size 2025-05-18 20:31:17 -04:00
Vishnu Sreekumaran Nair de1d1f8cad update infrastructure settings page font size 2025-05-18 20:27:56 -04:00
Vishnu Sreekumaran Nair 1cb1f48cdc update pagespeed create/configure page font size 2025-05-18 20:25:07 -04:00
Vishnu Sreekumaran Nair 02e48ad8da update uptime configure page font size 2025-05-18 20:18:02 -04:00
Vishnu Sreekumaran Nair 8e6eeabb48 update uptime settings page font size 2025-05-18 20:14:37 -04:00
remememe c22a0cea99 Merge branch 'develop' of https://github.com/remememe/Checkmate into develop 2025-05-18 14:30:09 +02:00
remememe d13c4a5531 fix: status page features checkboxes not saving 2025-05-18 13:54:51 +02:00
dependabot[bot] 4b950c8a83 Bump vite from 5.4.18 to 5.4.19 in /client
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.18 to 5.4.19.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.19/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.19/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 5.4.19
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-17 11:00:43 +00:00
Owaise Imdad cf344b1809 Email test button added in details page. 2025-05-17 11:51:31 +05:30
mohadeseh safari aaba9e8d42 Translation Fix 2025-05-16 19:59:19 -04:00
mohadeseh safari dbcdb24da6 feat: Enhance useSendTestEmail hook to support testing without saving
- Add support for optional email configuration parameter
- Implement improved error handling with detailed messages
- Add internationalization support for all user feedback
- Return appropriate error information for better UX
- Maintain backward compatibility with existing code
2025-05-16 19:45:24 -04:00
mohadeseh safari 083c395f10 feat: Update email testing to use form values without saving
- Modify test email button to collect current form values
- Add validation for required fields before testing
- Pass configuration directly to test endpoint
- Improve user experience by allowing testing before saving
2025-05-16 19:29:44 -04:00
mohadeseh safari 8ef45a7fb9 feat: Update email testing to use form values without saving
- Update sendTestEmail method to use new /api/v1/settings/test-email endpoint
- Send current form values directly to test endpoint without saving to DB
- Format request body according to backend requirements
2025-05-16 18:24:34 -04:00
Alex Holliday 5872879c9a inject email service into settings controller 2025-05-16 15:02:26 -07:00
Alex Holliday f6a2a66386 allow optional transport params 2025-05-16 15:02:07 -07:00
Alex Holliday d71181e65b add test-email body validation 2025-05-16 15:01:45 -07:00
Alex Holliday bea8ddb330 add a test email route to settings routes 2025-05-16 15:01:34 -07:00
Alex Holliday 0906e883c5 add test email controller method 2025-05-16 15:01:06 -07:00
Alexander Holliday e7694afeba Merge pull request #2292 from bluewave-labs/fix/bump-package-lock
update package-lock
2025-05-16 14:19:29 -07:00
Alex Holliday 9d69bd6782 update package-lock 2025-05-16 14:18:17 -07:00
Alex Holliday e6a387b8b2 refactor job queue 2025-05-16 14:09:49 -07:00
Alex Holliday b471c49cbf unordered bulk insert 2025-05-16 14:08:45 -07:00
Alex Holliday 79abb0fd84 rename methods 2025-05-16 14:07:47 -07:00
Alexander Holliday 69bcd07c01 Merge pull request #2290 from bluewave-labs/hotfix/bulk-insert-error-handling
hotfix: graceful error hanlding, unordered write
2025-05-16 09:55:24 -07:00
Alex Holliday 4a07c4f985 graceful error hanlding, unordered write 2025-05-16 09:54:35 -07:00
Alexander Holliday 443c002c31 Merge pull request #2281 from Anurag123Chauhan/fix/error-msg-change
Changed error msg of invalid of Docker container ID
2025-05-16 09:16:51 -07:00
Anurag123Chauhan fcfb225410 final indentation 2025-05-16 19:32:34 +05:30
Anurag123Chauhan ed03c17c22 last 2025-05-16 19:20:16 +05:30
Anurag123Chauhan 1d6a5a4442 Indent change 2025-05-16 19:16:52 +05:30
Anurag123Chauhan f415ca543b indent changes 2025-05-16 19:07:16 +05:30
Alexander Holliday f7beb4db0e Merge pull request #2287 from bluewave-labs/hotfix/uptime-perentage
hotfix: uptime-percentage
2025-05-15 14:21:30 -07:00
Alex Holliday 3fda54ca41 hotfix/uptime-percentage 2025-05-15 14:20:24 -07:00
Alexander Holliday 5ba352028d Merge pull request #2284 from bluewave-labs/feat/unified-details-header
feat: unified details header
2025-05-15 12:22:48 -07:00
Alexander Holliday 0fdb0a9e05 Merge pull request #2283 from bluewave-labs/fix/email-settings-strings
fix: update strings for email settings
2025-05-15 12:20:14 -07:00
Alex Holliday 93fab04d7c add jsdoc 2025-05-15 12:19:39 -07:00
Alexander Holliday 27649b33c0 Merge pull request #2285 from Br0wnHammer/fix/fe/infra-uptime-percent
Fix: Infra Uptime Percentage
2025-05-15 12:00:31 -07:00
Br0wnHammer 591fe567ec Fix: Infra Uptime Percentage 2025-05-16 00:23:50 +05:30
Alex Holliday eb67c7edff remove log statement 2025-05-15 11:47:40 -07:00
Alex Holliday 03a915a915 add loading state to pause button 2025-05-15 11:19:24 -07:00
Alex Holliday 6db850dd4d remove monitor from hook 2025-05-15 11:18:46 -07:00
Alex Holliday 85a428dd92 fix backwards monitor state 2025-05-15 10:51:21 -07:00
Alex Holliday 24cf4896aa use new header 2025-05-15 10:51:06 -07:00
Alex Holliday f9fcd4ff3a remove pause from uptime config page 2025-05-15 10:50:21 -07:00
Alex Holliday 54db96a03e add trigger to useFetchUptimeMonitorDetails 2025-05-15 10:49:38 -07:00
Alex Holliday 9263f9f950 add unified control header for all monitor details pages 2025-05-15 10:48:54 -07:00
Alex Holliday 04f75efa11 add hook for monitor contolrs 2025-05-15 10:48:32 -07:00
Alex Holliday a6e900e52a update strings for email settings 2025-05-15 09:49:24 -07:00
Alexander Holliday d48d3b9ff4 Merge pull request #2271 from catilgan-nextension/fix/nodemailer_relay
Enabling SMTP Relay with Nodemailer
2025-05-15 09:39:01 -07:00
Cahit Atilgan 8888a007f2 fix: added email connection validation (server side) 2025-05-15 15:30:04 +02:00
Cahit Atilgan e6b823d89e fix: added email connection validation 2025-05-15 15:16:36 +02:00
Cahit Atilgan 7327f24c47 fix: added email connection validation 2025-05-15 14:38:20 +02:00
Cahit Atilgan a3a381aad1 fix: removed env variable, configured a form field for editing email connect host 2025-05-15 14:27:08 +02:00
Cahit Atilgan 2b046dfe18 Merge branch 'develop' into fix/nodemailer_relay 2025-05-15 09:34:38 +02:00
Anurag123Chauhan 9da9ef6939 recommended changes 2025-05-15 11:16:02 +05:30
Anurag123Chauhan 18b439bad6 added msg 2025-05-15 11:05:45 +05:30
Anurag123Chauhan 0f91d3b6e1 fix: separate Docker ID and URL validation error messages 2025-05-15 11:03:05 +05:30
Anurag123Chauhan edad05b63b Changed error msg of invalid of Docker container ID 2025-05-15 10:48:04 +05:30
Alexander Holliday 54d7cbb121 Merge pull request #2279 from Br0wnHammer/fix/fe/port-validation
Fix: Port Validation Uptime
2025-05-14 14:31:54 -07:00
Br0wnHammer 3c48eacc98 Fix: Port Validation Uptime 2025-05-15 02:38:26 +05:30
Alexander Holliday 7964e731e4 Merge pull request #2278 from bluewave-labs/develop
hotfix: remove arm support
2025-05-14 12:17:43 -07:00
Alexander Holliday 8ffaaea28d Merge pull request #2277 from bluewave-labs/fix/remove-arm64-config
hotfix: remove arm64 config
2025-05-14 12:17:04 -07:00
Alex Holliday 8585500e7c remove build platform target 2025-05-14 12:16:09 -07:00
Alex Holliday 8a0725276b remove buildx 2025-05-14 12:10:16 -07:00
Alex Holliday a0f41b03ba add build platform target 2025-05-14 12:00:54 -07:00
Alexander Holliday 95743508c8 Merge pull request #2276 from bluewave-labs/develop
hotfix: notifications safe access
2025-05-14 10:44:52 -07:00
Alexander Holliday 3da5be3602 Merge pull request #2273 from fredrikburmester/develop
fix: TypeError in hardware notifications for null disk values
2025-05-14 10:43:52 -07:00
Fredrik Burmester f6e460d97a fix: typeof number check and more safety 2025-05-14 19:37:47 +02:00
Fredrik Burmester be20fb0dbb fix: TypeError in hardware notifications for null disk values 2025-05-14 16:08:18 +02:00
Cahit Atilgan e40b1f9c5b Merge branch 'develop' into fix/nodemailer_relay 2025-05-14 09:33:04 +02:00
Alexander Holliday 7a1b51a1fd Merge pull request #2268 from bluewave-labs/feat/buildx-arm
update workflow
2025-05-13 15:33:17 -07:00
Alex Holliday 077fae2147 update workflow 2025-05-13 15:32:28 -07:00
Alexander Holliday d825e6af7d Merge pull request #2267 from bluewave-labs/feat/buildx-arm
feat: build multi-arch images for dist
2025-05-13 15:29:48 -07:00
Alex Holliday 6dc5894b9a build multi-arch images for dist 2025-05-13 15:28:47 -07:00
Cahit Atilgan 12925c515a fix: configuring gmail smtp relay without any authentication 2025-05-13 18:07:53 +02:00
Cahit Atilgan 8c64221f88 fix: configuring gmail smtp relay without any authentication 2025-05-13 16:21:30 +02:00
152 changed files with 10373 additions and 4037 deletions

No files matched your search

+43 -23
View File
@@ -1,45 +1,60 @@
import axios from 'axios'; import axios from "axios";
import fs from 'fs-extra'; import fs from "fs-extra";
import path from 'path'; import path from "path";
import { URLSearchParams } from 'url'; import { URLSearchParams } from "url";
// POEditor API information // POEditor API information
const API_TOKEN = process.env.POEDITOR_API; const API_TOKEN = process.env.POEDITOR_API;
const PROJECT_ID = process.env.POEDITOR_PROJECT_ID; const PROJECT_ID = process.env.POEDITOR_PROJECT_ID;
const LANGUAGES = (process.env.LANGUAGES || 'tr,en').split(','); const LANGUAGES = (process.env.LANGUAGES || "tr,en").split(",");
const EXPORT_FORMAT = process.env.EXPORT_FORMAT || 'key_value_json'; const EXPORT_FORMAT = process.env.EXPORT_FORMAT || "key_value_json";
// POEditor API endpoint // POEditor API endpoint
const API_URL = 'https://api.poeditor.com/v2'; const API_URL = "https://api.poeditor.com/v2";
function normalizeLanguageCode(language) {
if (language.includes("-")) {
const [base, region] = language.split("-");
return `${base}-${region.toUpperCase()}`;
}
return language;
}
// Function to download translations // Function to download translations
async function downloadTranslations() { async function downloadTranslations() {
try { try {
console.log('Downloading translations from POEditor...'); console.log("Downloading translations from POEditor...");
console.log(`Using export format: ${EXPORT_FORMAT}`); console.log(`Using export format: ${EXPORT_FORMAT}`);
for (const language of LANGUAGES) { for (const language of LANGUAGES) {
console.log(`Downloading translations for ${language} language...`); console.log(`Downloading translations for ${language} language...`);
// Get export URL from POEditor // Get export URL from POEditor
const exportResponse = await axios.post(`${API_URL}/projects/export`, const exportResponse = await axios.post(
`${API_URL}/projects/export`,
new URLSearchParams({ new URLSearchParams({
api_token: API_TOKEN, api_token: API_TOKEN,
id: PROJECT_ID, id: PROJECT_ID,
language: language, language: language,
type: EXPORT_FORMAT type: EXPORT_FORMAT,
}) })
); );
if (exportResponse.data.response.status !== 'success') { if (exportResponse.data.response.status !== "success") {
throw new Error(`Failed to get export URL for ${language} language: ${JSON.stringify(exportResponse.data)}`); throw new Error(
`Failed to get export URL for ${language} language: ${JSON.stringify(
exportResponse.data
)}`
);
} }
const fileUrl = exportResponse.data.result.url; const fileUrl = exportResponse.data.result.url;
console.log(`Export URL obtained for ${language}`); console.log(`Export URL obtained for ${language}`);
// Download translation file // Download translation file
const downloadResponse = await axios.get(fileUrl, { responseType: 'json' }); const downloadResponse = await axios.get(fileUrl, {
responseType: "json",
});
const translations = downloadResponse.data; const translations = downloadResponse.data;
console.log(`Downloaded translations for ${language}`); console.log(`Downloaded translations for ${language}`);
@@ -48,9 +63,11 @@ async function downloadTranslations() {
// If data is in array format, convert it to key-value format // If data is in array format, convert it to key-value format
if (Array.isArray(translations)) { if (Array.isArray(translations)) {
console.log(`Converting array format to key-value format for ${language}`); console.log(
`Converting array format to key-value format for ${language}`
);
formattedTranslations = {}; formattedTranslations = {};
translations.forEach(item => { translations.forEach((item) => {
if (item.term && item.definition) { if (item.term && item.definition) {
formattedTranslations[item.term] = item.definition; formattedTranslations[item.term] = item.definition;
} }
@@ -58,16 +75,19 @@ async function downloadTranslations() {
} }
// Determine the output filename based on language // Determine the output filename based on language
const filename = language === 'en' ? 'gb.json' : `${language}.json`; const normalizedLanguage = normalizeLanguageCode(language);
const outputPath = path.join(process.cwd(), 'temp', filename); const filename = `${normalizedLanguage}.json`;
const outputPath = path.join(process.cwd(), "temp", filename);
await fs.writeJson(outputPath, formattedTranslations, { spaces: 2 }); await fs.writeJson(outputPath, formattedTranslations, { spaces: 2 });
console.log(`Translations for ${language} language successfully downloaded and saved as: ${filename}`); console.log(
`Translations for ${language} language successfully downloaded and saved as: ${filename}`
);
} }
console.log('All translations successfully downloaded!'); console.log("All translations successfully downloaded!");
} catch (error) { } catch (error) {
console.error('An error occurred while downloading translations:', error); console.error("An error occurred while downloading translations:", error);
process.exit(1); process.exit(1);
} }
} }
@@ -76,15 +96,15 @@ async function downloadTranslations() {
async function main() { async function main() {
try { try {
// Clean temp folder // Clean temp folder
await fs.emptyDir(path.join(process.cwd(), 'temp')); await fs.emptyDir(path.join(process.cwd(), "temp"));
// Download translations // Download translations
await downloadTranslations(); await downloadTranslations();
} catch (error) { } catch (error) {
console.error('An error occurred during the process:', error); console.error("An error occurred during the process:", error);
process.exit(1); process.exit(1);
} }
} }
// Run script // Run script
main(); main();
@@ -36,6 +36,9 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry - name: Log in to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
@@ -0,0 +1,44 @@
name: Distribution deploy - Monolithic Multiarch
on:
push:
branches: ["master"]
workflow_dispatch:
jobs:
docker-build-and-push-server:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push multi-arch Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./docker/dist-arm/server.Dockerfile
push: true
tags: ghcr.io/bluewave-labs/checkmate:backend-dist-mono-multiarch
platforms: linux/amd64,linux/arm64
labels: |
org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate
# - name: Build Server Docker image
# run: |
# docker build \
# -t ghcr.io/bluewave-labs/checkmate:backend-dist-mono-multiarch \
# -f ./docker/dist-arm/server.Dockerfile \
# --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
# .
# - name: Push Server Docker image
# run: docker push ghcr.io/bluewave-labs/checkmate:backend-dist-mono-multiarch
@@ -0,0 +1,30 @@
name: Distribution deploy - Monolithic
on:
push:
branches: ["master"]
workflow_dispatch:
jobs:
docker-build-and-push-server:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Server Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:backend-dist-mono \
-f ./docker/dist-mono/server.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
.
- name: Push Server Docker image
run: docker push ghcr.io/bluewave-labs/checkmate:backend-dist-mono
+7 -2
View File
@@ -7,13 +7,12 @@ on:
languages: languages:
description: "Languages to synchronize (comma separated, e.g.: tr,en,es)" description: "Languages to synchronize (comma separated, e.g.: tr,en,es)"
required: false required: false
default: "tr,en" default: "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr"
format: format:
description: "Export format (key_value_json or json)" description: "Export format (key_value_json or json)"
required: false required: false
default: "key_value_json" default: "key_value_json"
permissions: permissions:
contents: write contents: write
pull-requests: write pull-requests: write
@@ -93,6 +92,12 @@ jobs:
id: date id: date
run: echo "date=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT run: echo "date=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT
- name: Format client code
run: |
cd client
npm ci
npm run format
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@v7 uses: peter-evans/create-pull-request@v7
with: with:
+11 -12
View File
@@ -56,7 +56,7 @@ If you have any questions, suggestions or comments, please use our [Discord chan
## 🧩 Features ## 🧩 Features
- Completely open source, deployable on your servers - Completely open source, deployable on your servers or home devices (e.g Raspberry Pi 4 or 5)
- Website monitoring - Website monitoring
- Page speed 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 etc) - requires [Capture](https://github.com/bluewave-labs/capture)
@@ -65,18 +65,17 @@ If you have any questions, suggestions or comments, please use our [Discord chan
- SSL monitoring - SSL monitoring
- Port monitoring - Port monitoring
- Incidents at a glance - Incidents at a glance
- E-mail notifications - Status pages
- E-mail, Webhooks, Discord, Telegram, Slack notifications
- Scheduled maintenance - Scheduled maintenance
- JSON query monitoring
- Support for multiple languages
**Short term roadmap:** **Short term roadmap:** ([Milestone 2.2](https://github.com/bluewave-labs/Checkmate/milestone/8))
- Status pages (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1131 - Better notifications
- Translations (i18n) (**in progress**) - Network monitoring
- Better notification options (Webhooks, Discord, Telegram, Slack) (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1545 - ..and a few more features
- JSON query monitoring (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1573
- Tagging/grouping monitors https://github.com/bluewave-labs/Checkmate/issues/1546
- More configuration options
- DNS monitoring
## 🏗️ Screenshots ## 🏗️ Screenshots
@@ -108,9 +107,9 @@ If you have any questions, suggestions or comments, please use our [Discord chan
## 🤝 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), [Vishnu](http://github.com/vishnusn77), [Mohadeseh](http://github.com/mohicody), [Gorkem](http://github.com/gorkem-bwl/), [Owaise](http://github.com/Owaiseimdad), [Aryaman](https://github.com/Br0wnHammer) and [Mert](https://github.com/mertssmnoglu) helping individuals and businesses monitor their infra and servers.
We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 5000+ stars and attracted 60 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 5800+ stars and attracted 70+ 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! 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!
-1
View File
@@ -1,2 +1 @@
This directory contains the client side (frontend) of Checkmate. This directory contains the client side (frontend) of Checkmate.
+20 -13
View File
@@ -1,17 +1,24 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8" />
<link
rel="icon"
href="./checkmate_favicon.svg"
/>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<head> <title>Checkmate</title>
<meta charset="UTF-8" /> </head>
<link rel="icon" href="./checkmate_favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Checkmate</title> <body>
</head> <div id="root"></div>
<script
<body> type="module"
<div id="root"></div> src="/src/main.jsx"
<script type="module" src="/src/main.jsx"></script> ></script>
</body> </body>
</html>
</html>
+1394 -509
View File
File diff suppressed because it is too large. Load diff
+11 -6
View File
@@ -8,28 +8,30 @@
"build": "vite build", "build": "vite build",
"build-dev": "vite build --mode development", "build-dev": "vite build --mode development",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview" "preview": "vite preview",
"format": "prettier --write .",
"format-check": "prettier --check ."
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.13.3", "@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0", "@emotion/styled": "^11.13.0",
"@fontsource/roboto": "^5.0.13", "@fontsource/roboto": "^5.0.13",
"@hello-pangea/dnd": "^18.0.0", "@hello-pangea/dnd": "^18.0.0",
"@mui/x-charts": "^7.5.1",
"@mui/x-data-grid": "7.29.0",
"@mui/x-date-pickers": "7.29.0",
"@mui/icons-material": "6.4.11", "@mui/icons-material": "6.4.11",
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d", "@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
"@mui/material": "6.4.11", "@mui/material": "6.4.11",
"@mui/x-charts": "^7.5.1",
"@mui/x-data-grid": "7.29.0",
"@mui/x-date-pickers": "7.29.0",
"@reduxjs/toolkit": "2.7.0", "@reduxjs/toolkit": "2.7.0",
"axios": "^1.7.4", "axios": "^1.7.4",
"dayjs": "1.11.13", "dayjs": "1.11.13",
"flag-icons": "7.3.2", "flag-icons": "7.3.2",
"jwt-decode": "^4.0.0",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"i18next": "^24.2.2", "i18next": "^24.2.2",
"immutability-helper": "^3.1.1", "immutability-helper": "^3.1.1",
"joi": "17.13.3", "joi": "17.13.3",
"jwt-decode": "^4.0.0",
"maplibre-gl": "5.3.1", "maplibre-gl": "5.3.1",
"mui-color-input": "^6.0.0", "mui-color-input": "^6.0.0",
"react": "18.3.1", "react": "18.3.1",
@@ -63,6 +65,9 @@
"eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.6", "eslint-plugin-react-refresh": "^0.4.6",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"vite": "^5.2.0" "vite": "^5.4.19"
},
"optionalDependencies": {
"@rollup/rollup-linux-arm64-musl": "4.41.0"
} }
} }
+3 -5
View File
@@ -1,7 +1,5 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [ "extends": ["config:recommended"],
"config:recommended" "labels": ["dependencies"]
],
"labels": ["dependencies"]
} }
@@ -3,28 +3,28 @@ import Button from "@mui/material/Button";
import { styled } from "@mui/material/styles"; import { styled } from "@mui/material/styles";
const RoundGradientButton = styled(Button)(({ theme }) => ({ const RoundGradientButton = styled(Button)(({ theme }) => ({
position: "relative", position: "relative",
border: "5px solid transparent", border: "5px solid transparent",
backgroundClip: "padding-box", backgroundClip: "padding-box",
borderRadius: 30, borderRadius: 30,
fontSize: "1.2rem", fontSize: "1.2rem",
color: theme.palette.primary.contrastText, color: theme.palette.primary.contrastText,
backgroundColor: theme.palette.background.main, backgroundColor: theme.palette.background.main,
"&:after": { "&:after": {
position: "absolute", position: "absolute",
top: -3, top: -3,
left: -3, left: -3,
right: -3, right: -3,
bottom: -3, bottom: -3,
background: background:
theme.palette.mode === "dark" theme.palette.mode === "dark"
? "linear-gradient(90deg, #842bd2, #ff5451, #8c52ff)" ? "linear-gradient(90deg, #842bd2, #ff5451, #8c52ff)"
: "linear-gradient(90deg, #842bd2, #ff5451, #8c52ff)", : "linear-gradient(90deg, #842bd2, #ff5451, #8c52ff)",
content: '""', content: '""',
zIndex: -1, zIndex: -1,
borderRadius: 30, borderRadius: 30,
}, },
})); }));
export default RoundGradientButton; export default RoundGradientButton;
@@ -31,78 +31,76 @@ import IconBox from "../../IconBox";
*/ */
const EmptyView = ({ const EmptyView = ({
icon, icon,
header, header,
message = "No Data", message = "No Data",
headingLevel = "h2", headingLevel = "h2",
justifyContent = "flex-start", justifyContent = "flex-start",
height = "100%" height = "100%",
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
return ( return (
<Stack <Stack
flex={1} flex={1}
direction="row" direction="row"
sx={{ sx={{
backgroundColor: theme.palette.primary.main, backgroundColor: theme.palette.primary.main,
border: 1, border: 1,
borderStyle: "solid", borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast, borderColor: theme.palette.primary.lowContrast,
borderRadius: 2, borderRadius: 2,
borderTopRightRadius: 4, borderTopRightRadius: 4,
borderBottomRightRadius: 4, borderBottomRightRadius: 4,
}} }}
> >
<Stack <Stack
flex={1} flex={1}
alignItems="center" alignItems="center"
sx={{ sx={{
padding: theme.spacing(8), padding: theme.spacing(8),
justifyContent, justifyContent,
gap: theme.spacing(8), gap: theme.spacing(8),
height, height,
"& h2": { "& h2": {
color: theme.palette.primary.contrastTextSecondary, color: theme.palette.primary.contrastTextSecondary,
fontSize: 15, fontSize: 15,
fontWeight: 500, fontWeight: 500,
}, },
"& tspan, & text": { "& tspan, & text": {
fill: theme.palette.primary.contrastTextTertiary, fill: theme.palette.primary.contrastTextTertiary,
}, },
}} }}
> >
<Stack <Stack
alignSelf="flex-start" alignSelf="flex-start"
direction="row" direction="row"
alignItems="center" alignItems="center"
gap={theme.spacing(6)} gap={theme.spacing(6)}
> >
{icon && <IconBox>{icon}</IconBox>} {icon && <IconBox>{icon}</IconBox>}
{header && <Typography component="h2">{header}</Typography>} {header && <Typography component="h2">{header}</Typography>}
</Stack> </Stack>
<Stack <Stack
flex={1} flex={1}
justifyContent="center" justifyContent="center"
alignItems="center" alignItems="center"
> >
<Typography component={headingLevel}> <Typography component={headingLevel}>{message}</Typography>
{message} </Stack>
</Typography> </Stack>
</Stack> </Stack>
</Stack> );
</Stack>
);
}; };
EmptyView.propTypes = { EmptyView.propTypes = {
message: PropTypes.string, message: PropTypes.string,
icon: PropTypes.node, icon: PropTypes.node,
header: PropTypes.string, header: PropTypes.string,
headingLevel: PropTypes.oneOf(['h1', 'h2', 'h3']), headingLevel: PropTypes.oneOf(["h1", "h2", "h3"]),
justifyContent: PropTypes.string, justifyContent: PropTypes.string,
height: PropTypes.string height: PropTypes.string,
}; };
export default EmptyView; export default EmptyView;
@@ -18,7 +18,13 @@ const ChartBox = ({
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
if (isEmpty) { if (isEmpty) {
return <EmptyView icon={icon} header={header} message={noDataMessage} />; return (
<EmptyView
icon={icon}
header={header}
message={noDataMessage}
/>
);
} }
return ( return (
<Stack <Stack
@@ -95,5 +101,5 @@ ChartBox.propTypes = {
header: PropTypes.string, header: PropTypes.string,
height: PropTypes.string, height: PropTypes.string,
noDataMessage: PropTypes.string, noDataMessage: PropTypes.string,
isEmpty: PropTypes.bool isEmpty: PropTypes.bool,
}; };
+246 -205
View File
@@ -1,214 +1,255 @@
import * as React from 'react'; import * as React from "react";
import Box from '@mui/material/Box'; import Box from "@mui/material/Box";
import Container from '@mui/material/Container'; import Container from "@mui/material/Container";
import IconButton from '@mui/material/IconButton'; import IconButton from "@mui/material/IconButton";
import Link from '@mui/material/Link'; import Link from "@mui/material/Link";
import Stack from '@mui/material/Stack'; import Stack from "@mui/material/Stack";
import Typography from '@mui/material/Typography'; import Typography from "@mui/material/Typography";
import { FaFacebook, FaLinkedin, FaGithub, FaTwitter, FaEnvelope } from 'react-icons/fa'; import { FaFacebook, FaLinkedin, FaGithub, FaTwitter, FaEnvelope } from "react-icons/fa";
function Copyright() { function Copyright() {
return ( return (
<Typography sx={{ color: 'text.secondary', mt: 1 }}> <Typography sx={{ color: "text.secondary", mt: 1 }}>
{'Copyright © '} {"Copyright © "}
<Link color="text.secondary" href="https://prism.uprock.com/"> <Link
UpRock color="text.secondary"
</Link> href="https://prism.uprock.com/"
&nbsp; >
{new Date().getFullYear()} UpRock
</Typography> </Link>
); &nbsp;
{new Date().getFullYear()}
</Typography>
);
} }
export default function Footer() { export default function Footer() {
return ( return (
<Container <Container
sx={{ sx={{
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
alignItems: 'center', alignItems: "center",
gap: { xs: 4, sm: 8 }, gap: { xs: 4, sm: 8 },
py: { xs: 24, sm: 24 }, py: { xs: 24, sm: 24 },
px: { xs: 12, sm: 12 }, px: { xs: 12, sm: 12 },
textAlign: { sm: 'center', md: 'left' }, textAlign: { sm: "center", md: "left" },
}} }}
> >
<Box
<Box sx={{
sx={{ display: "flex",
display: 'flex', justifyContent: "space-between",
justifyContent: 'space-between', pt: { xs: 4, sm: 8 },
pt: { xs: 4, sm: 8 }, width: "100%",
width: '100%', borderColor: "divider",
borderColor: 'divider', }}
}} >
> <div>
<div> <Link
<Link color="text.secondary" href="https://uprock.com/privacy-policy"> color="text.secondary"
Privacy Policy href="https://uprock.com/privacy-policy"
</Link> >
<Typography sx={{ display: 'inline', mx: 0.5, opacity: 0.5 }}> Privacy Policy
&nbsp;•&nbsp; </Link>
</Typography> <Typography sx={{ display: "inline", mx: 0.5, opacity: 0.5 }}>
<Link color="text.secondary" href="https://uprock.com/terms-of-use"> &nbsp;•&nbsp;
Terms of Service </Typography>
</Link> <Link
<Copyright /> color="text.secondary"
</div> href="https://uprock.com/terms-of-use"
<Stack >
direction="row" Terms of Service
spacing={1} </Link>
useFlexGap <Copyright />
sx={{ justifyContent: 'left', color: 'text.secondary' }} </div>
> <Stack
<IconButton direction="row"
color="inherit" spacing={1}
size="small" useFlexGap
href="mailto:prism@uprock.com?subject=Interested%20in%20UpRock%20Prism" sx={{ justifyContent: "left", color: "text.secondary" }}
aria-label="Contact Us" >
sx={{ alignSelf: 'center' }} <IconButton
> color="inherit"
<FaEnvelope /> size="small"
</IconButton> href="mailto:prism@uprock.com?subject=Interested%20in%20UpRock%20Prism"
<IconButton aria-label="Contact Us"
color="inherit" sx={{ alignSelf: "center" }}
size="small" >
href="https://facebook.com/uprockcom" <FaEnvelope />
aria-label="Facebook" </IconButton>
sx={{ alignSelf: 'center' }} <IconButton
> color="inherit"
<FaFacebook /> size="small"
</IconButton> href="https://facebook.com/uprockcom"
<IconButton aria-label="Facebook"
color="inherit" sx={{ alignSelf: "center" }}
size="small" >
href="https://x.com/uprockcom" <FaFacebook />
aria-label="X" </IconButton>
sx={{ alignSelf: 'center' }} <IconButton
> color="inherit"
<FaTwitter /> size="small"
</IconButton> href="https://x.com/uprockcom"
<IconButton aria-label="X"
color="inherit" sx={{ alignSelf: "center" }}
size="small" >
href="https://www.linkedin.com/company/uprock/" <FaTwitter />
aria-label="LinkedIn" </IconButton>
sx={{ alignSelf: 'center' }} <IconButton
> color="inherit"
<FaLinkedin /> size="small"
</IconButton> href="https://www.linkedin.com/company/uprock/"
<IconButton aria-label="LinkedIn"
color="inherit" sx={{ alignSelf: "center" }}
size="small" >
href="https://github.com/uprockcom" <FaLinkedin />
aria-label="GitHub" </IconButton>
sx={{ alignSelf: 'center' }} <IconButton
> color="inherit"
<FaGithub /> size="small"
</IconButton> href="https://github.com/uprockcom"
aria-label="GitHub"
sx={{ alignSelf: "center" }}
>
<FaGithub />
</IconButton>
</Stack>
</Box>
</Stack> <Box
</Box> sx={{
display: "flex",
<Box flexDirection: "column",
sx={{ alignItems: "center",
display: 'flex', mt: 4,
flexDirection: 'column', }}
alignItems: 'center', >
mt: 4, <Typography
}} variant="h2"
> sx={{ color: "text.secondary" }}
<Typography variant="h2" sx={{ color: 'text.secondary' }}> >
Made with ❤️ by&nbsp; Made with ❤️ by&nbsp;
<Link href="https://uprock.com" color="inherit" sx={{ mx: 0.5 }}> <Link
UpRock&nbsp; href="https://uprock.com"
</Link> color="inherit"
&&nbsp; sx={{ mx: 0.5 }}
<Link href="https://bluewavelabs.ca" color="inherit" sx={{ mx: 0.5 }}> >
Bluewave Labs UpRock&nbsp;
</Link> </Link>
</Typography> &&nbsp;
<Box <Link
sx={{ href="https://bluewavelabs.ca"
display: 'flex', color="inherit"
alignItems: 'center', sx={{ mx: 0.5 }}
mt: 2, >
}} Bluewave Labs
> </Link>
<Typography variant="h2" sx={{ color: 'text.secondary', mr: 1 }}> </Typography>
Built on&nbsp; <Box
</Typography> sx={{
<svg display: "flex",
id="Layer_1" alignItems: "center",
xmlns="http://www.w3.org/2000/svg" mt: 2,
xmlnsXlink="http://www.w3.org/1999/xlink" }}
x="0px" >
y="0px" <Typography
viewBox="0 0 397.7 311.7" variant="h2"
xmlSpace="preserve" sx={{ color: "text.secondary", mr: 1 }}
width="15" >
height="15" Built on&nbsp;
> </Typography>
<style type="text/css"> <svg
{`.st0{fill:url(#SVGID_1_);} id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 397.7 311.7"
xmlSpace="preserve"
width="15"
height="15"
>
<style type="text/css">
{`.st0{fill:url(#SVGID_1_);}
.st1{fill:url(#SVGID_2_);} .st1{fill:url(#SVGID_2_);}
.st2{fill:url(#SVGID_3_);}`} .st2{fill:url(#SVGID_3_);}`}
</style> </style>
<linearGradient <linearGradient
id="SVGID_1_" id="SVGID_1_"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
x1="360.8791" x1="360.8791"
y1="351.4553" y1="351.4553"
x2="141.213" x2="141.213"
y2="-69.2936" y2="-69.2936"
gradientTransform="matrix(1 0 0 -1 0 314)" gradientTransform="matrix(1 0 0 -1 0 314)"
> >
<stop offset="0" style={{ stopColor: 'rgb(0, 255, 163)' }} /> <stop
<stop offset="1" style={{ stopColor: 'rgb(220, 31, 255)' }} /> offset="0"
</linearGradient> style={{ stopColor: "rgb(0, 255, 163)" }}
<path />
className="st0" <stop
d="M64.6,237.9c2.4-2.4,5.7-3.8,9.2-3.8h317.4c5.8,0,8.7,7,4.6,11.1l-62.7,62.7c-2.4,2.4-5.7,3.8-9.2,3.8H6.5 c-5.8,0-8.7-7-4.6-11.1L64.6,237.9z" offset="1"
/> style={{ stopColor: "rgb(220, 31, 255)" }}
<linearGradient />
id="SVGID_2_" </linearGradient>
gradientUnits="userSpaceOnUse" <path
x1="264.8291" className="st0"
y1="401.6014" d="M64.6,237.9c2.4-2.4,5.7-3.8,9.2-3.8h317.4c5.8,0,8.7,7,4.6,11.1l-62.7,62.7c-2.4,2.4-5.7,3.8-9.2,3.8H6.5 c-5.8,0-8.7-7-4.6-11.1L64.6,237.9z"
x2="45.163" />
y2="-19.1475" <linearGradient
gradientTransform="matrix(1 0 0 -1 0 314)" id="SVGID_2_"
> gradientUnits="userSpaceOnUse"
<stop offset="0" style={{ stopColor: 'rgb(0, 255, 163)' }} /> x1="264.8291"
<stop offset="1" style={{ stopColor: 'rgb(220, 31, 255)' }} /> y1="401.6014"
</linearGradient> x2="45.163"
<path y2="-19.1475"
className="st1" gradientTransform="matrix(1 0 0 -1 0 314)"
d="M64.6,3.8C67.1,1.4,70.4,0,73.8,0h317.4c5.8,0,8.7,7,4.6,11.1l-62.7,62.7c-2.4,2.4-5.7,3.8-9.2,3.8H6.5 c-5.8,0-8.7-7-4.6-11.1L64.6,3.8z" >
/> <stop
<linearGradient offset="0"
id="SVGID_3_" style={{ stopColor: "rgb(0, 255, 163)" }}
gradientUnits="userSpaceOnUse" />
x1="312.5484" <stop
y1="376.688" offset="1"
x2="92.8822" style={{ stopColor: "rgb(220, 31, 255)" }}
y2="-44.061" />
gradientTransform="matrix(1 0 0 -1 0 314)" </linearGradient>
> <path
<stop offset="0" style={{ stopColor: 'rgb(0, 255, 163)' }} /> className="st1"
<stop offset="1" style={{ stopColor: 'rgb(220, 31, 255)' }} /> d="M64.6,3.8C67.1,1.4,70.4,0,73.8,0h317.4c5.8,0,8.7,7,4.6,11.1l-62.7,62.7c-2.4,2.4-5.7,3.8-9.2,3.8H6.5 c-5.8,0-8.7-7-4.6-11.1L64.6,3.8z"
</linearGradient> />
<path <linearGradient
className="st2" id="SVGID_3_"
d="M333.1,120.1c-2.4-2.4-5.7-3.8-9.2-3.8H6.5c-5.8,0-8.7,7-4.6,11.1l62.7,62.7c2.4,2.4,5.7,3.8,9.2,3.8h317.4 c5.8,0,8.7-7,4.6-11.1L333.1,120.1z" gradientUnits="userSpaceOnUse"
/> x1="312.5484"
</svg> y1="376.688"
<Typography variant="h2" sx={{ color: 'text.secondary', ml: 1 }}> x2="92.8822"
&nbsp;Solana y2="-44.061"
</Typography> gradientTransform="matrix(1 0 0 -1 0 314)"
</Box> >
</Box> <stop
</Container> offset="0"
); style={{ stopColor: "rgb(0, 255, 163)" }}
/>
<stop
offset="1"
style={{ stopColor: "rgb(220, 31, 255)" }}
/>
</linearGradient>
<path
className="st2"
d="M333.1,120.1c-2.4-2.4-5.7-3.8-9.2-3.8H6.5c-5.8,0-8.7,7-4.6,11.1l62.7,62.7c2.4,2.4,5.7,3.8,9.2,3.8h317.4 c5.8,0,8.7-7,4.6-11.1L333.1,120.1z"
/>
</svg>
<Typography
variant="h2"
sx={{ color: "text.secondary", ml: 1 }}
>
&nbsp;Solana
</Typography>
</Box>
</Box>
</Container>
);
} }
+1 -1
View File
@@ -29,7 +29,7 @@ const ConfigBox = styled(Stack)(({ theme }) => ({
}, },
"& h1, & h2": { "& h1, & h2": {
color: theme.palette.primary.contrastTextSecondary, color: theme.palette.primary.contrastTextSecondary,
} },
})); }));
export default ConfigBox; export default ConfigBox;
+31 -22
View File
@@ -7,7 +7,7 @@ import ConfigBox from "../ConfigBox";
* ConfigRow is a styled container used to layout content in a row format with specific padding, border, and spacing. * ConfigRow is a styled container used to layout content in a row format with specific padding, border, and spacing.
* It serves as the wrapper for ConfigBox, with the left section displaying the title and description, * It serves as the wrapper for ConfigBox, with the left section displaying the title and description,
* and the right section displaying the children. * and the right section displaying the children.
* *
* @component * @component
* @example * @example
* return ( * return (
@@ -19,31 +19,40 @@ import ConfigBox from "../ConfigBox";
*/ */
const ConfigRow = ({ title, description, children }) => { const ConfigRow = ({ title, description, children }) => {
const theme = useTheme(); const theme = useTheme();
return ( return (
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h2" variant="h2"> <Typography
{title} component="h2"
</Typography> variant="h2"
{description && ( >
<Typography variant="body2" mt={theme.spacing(2)}> {title}
{description} </Typography>
</Typography> {description && (
)} <Typography
</Box> variant="body2"
<Stack gap={theme.spacing(15)} mt={theme.spacing(4)}> mt={theme.spacing(2)}
{children} >
</Stack> {description}
</ConfigBox> </Typography>
); )}
</Box>
<Stack
gap={theme.spacing(15)}
mt={theme.spacing(4)}
>
{children}
</Stack>
</ConfigBox>
);
}; };
ConfigRow.propTypes = { ConfigRow.propTypes = {
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
description: PropTypes.string, description: PropTypes.string,
children: PropTypes.node, children: PropTypes.node,
}; };
export default ConfigRow; export default ConfigRow;
+8 -4
View File
@@ -42,10 +42,14 @@ const FilterHeader = ({ header, options, value, onChange, multiple = true }) =>
return header; return header;
} }
return header + " | " + selected return (
.map((value) => options.find((option) => option.value === value)?.label) header +
.filter(Boolean) " | " +
.join(", "); selected
.map((value) => options.find((option) => option.value === value)?.label)
.filter(Boolean)
.join(", ")
);
}} }}
MenuProps={{ MenuProps={{
anchorOrigin: { anchorOrigin: {
+3 -2
View File
@@ -14,7 +14,7 @@ import Dot from "../Dot";
* @param {number} params.percentage - The percentage to display. * @param {number} params.percentage - The percentage to display.
* @returns {React.ElementType} Returns a div element with the host details. * @returns {React.ElementType} Returns a div element with the host details.
*/ */
const Host = ({ url, title, percentageColor, percentage }) => { const Host = ({ url, title, percentageColor, percentage, showURL }) => {
const theme = useTheme(); const theme = useTheme();
return ( return (
<Stack> <Stack>
@@ -40,7 +40,7 @@ const Host = ({ url, title, percentageColor, percentage }) => {
</> </>
)} )}
</Stack> </Stack>
<span style={{ opacity: 0.6 }}>{url}</span> {showURL && <span style={{ opacity: 0.6 }}>{url}</span>}
</Stack> </Stack>
); );
}; };
@@ -50,6 +50,7 @@ Host.propTypes = {
percentageColor: PropTypes.string, percentageColor: PropTypes.string,
percentage: PropTypes.string, percentage: PropTypes.string,
url: PropTypes.string, url: PropTypes.string,
showURL: PropTypes.bool,
}; };
export default Host; export default Host;
+184 -185
View File
@@ -36,199 +36,198 @@ const ImageUpload = ({
error, error,
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
const [uploadComplete, setUploadComplete] = useState(false); const [uploadComplete, setUploadComplete] = useState(false);
const [completedFile, setCompletedFile] = useState(null); const [completedFile, setCompletedFile] = useState(null);
const [file, setFile] = useState(null); const [file, setFile] = useState(null);
const [progress, setProgress] = useState({ value: 0, isLoading: false }); const [progress, setProgress] = useState({ value: 0, isLoading: false });
const intervalRef = useRef(null); const intervalRef = useRef(null);
const [localError, setLocalError] = useState(null); const [localError, setLocalError] = useState(null);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const roundStyle = previewIsRound ? { borderRadius: "50%" } : {}; const roundStyle = previewIsRound ? { borderRadius: "50%" } : {};
const handleImageChange = useCallback( const handleImageChange = useCallback(
(file) => { (file) => {
if (!file) return; if (!file) return;
const isValidType = accept.some((type) => const isValidType = accept.some((type) => file.type.includes(type));
file.type.includes(type) const isValidSize = file.size <= maxSize;
);
const isValidSize = file.size <= maxSize;
if (!isValidType) {
setLocalError(t('invalidFileFormat'));
return;
}
if (!isValidSize) {
setLocalError(t('invalidFileSize'));
return;
}
setLocalError(null);
const previewFile = {
src: URL.createObjectURL(file),
name: file.name,
file,
};
setFile(previewFile);
setProgress({ value: 0, isLoading: true });
intervalRef.current = setInterval(() => {
setProgress((prev) => {
const buffer = 12;
if (prev.value + buffer >= 100) {
clearInterval(intervalRef.current);
setUploadComplete(true);
setCompletedFile(previewFile);
return { value: 100, isLoading: false };
}
return { value: prev.value + buffer, isLoading: true };
});
}, 120);
},
[maxSize, accept]
);
useEffect(() => { if (!isValidType) {
if (uploadComplete && completedFile) { setLocalError(t("invalidFileFormat"));
onChange?.(completedFile); return;
setUploadComplete(false); }
setCompletedFile(null); if (!isValidSize) {
} setLocalError(t("invalidFileSize"));
}, [uploadComplete, completedFile, onChange]); return;
}
setLocalError(null);
const previewFile = {
src: URL.createObjectURL(file),
name: file.name,
file,
};
setFile(previewFile);
setProgress({ value: 0, isLoading: true });
intervalRef.current = setInterval(() => {
setProgress((prev) => {
const buffer = 12;
if (prev.value + buffer >= 100) {
clearInterval(intervalRef.current);
setUploadComplete(true);
setCompletedFile(previewFile);
return { value: 100, isLoading: false };
}
return { value: prev.value + buffer, isLoading: true };
});
}, 120);
},
[maxSize, accept]
);
useEffect(() => {
if (uploadComplete && completedFile) {
onChange?.(completedFile);
setUploadComplete(false);
setCompletedFile(null);
}
}, [uploadComplete, completedFile, onChange]);
return ( return (
<> <>
{src ? ( {src ? (
<Stack direction="row" justifyContent="center"> <Stack
<Image direction="row"
alt="Uploaded preview" justifyContent="center"
src={src} >
width="250px" <Image
height="250px" alt="Uploaded preview"
sx={{ ...roundStyle }} src={src}
/> width="250px"
</Stack> height="250px"
) : ( sx={{ ...roundStyle }}
<> />
<Box </Stack>
className="image-field-wrapper" ) : (
mt={theme.spacing(8)} <>
onDragEnter={() => setIsDragging(true)} <Box
onDragLeave={() => setIsDragging(false)} className="image-field-wrapper"
onDrop={() => setIsDragging(false)} mt={theme.spacing(8)}
sx={{ onDragEnter={() => setIsDragging(true)}
position: "relative", onDragLeave={() => setIsDragging(false)}
height: "fit-content", onDrop={() => setIsDragging(false)}
border: "dashed", sx={{
borderRadius: theme.shape.borderRadius, position: "relative",
borderColor: isDragging height: "fit-content",
? theme.palette.primary.main border: "dashed",
: theme.palette.primary.lowContrast, borderRadius: theme.shape.borderRadius,
backgroundColor: isDragging borderColor: isDragging
? "hsl(215, 87%, 51%, 0.05)" ? theme.palette.primary.main
: "transparent", : theme.palette.primary.lowContrast,
borderWidth: "2px", backgroundColor: isDragging ? "hsl(215, 87%, 51%, 0.05)" : "transparent",
transition: "0.2s", borderWidth: "2px",
"&:hover": { transition: "0.2s",
borderColor: theme.palette.primary.main, "&:hover": {
backgroundColor: "hsl(215, 87%, 51%, 0.05)", borderColor: theme.palette.primary.main,
}, backgroundColor: "hsl(215, 87%, 51%, 0.05)",
}} },
> }}
<TextField >
type="file" <TextField
onChange={(e) => handleImageChange(e?.target?.files?.[0])} type="file"
sx={{ onChange={(e) => handleImageChange(e?.target?.files?.[0])}
width: "100%", sx={{
"& .MuiInputBase-input[type='file']": { width: "100%",
opacity: 0, "& .MuiInputBase-input[type='file']": {
cursor: "pointer", opacity: 0,
maxWidth: "500px", cursor: "pointer",
minHeight: "175px", maxWidth: "500px",
zIndex: 1, minHeight: "175px",
}, zIndex: 1,
"& fieldset": { },
padding: 0, "& fieldset": {
border: "none", padding: 0,
}, border: "none",
}} },
/> }}
<Stack />
alignItems="center" <Stack
gap="4px" alignItems="center"
sx={{ gap="4px"
position: "absolute", sx={{
top: "50%", position: "absolute",
left: "50%", top: "50%",
transform: "translate(-50%, -50%)", left: "50%",
zIndex: 0, transform: "translate(-50%, -50%)",
width: "100%", zIndex: 0,
}} width: "100%",
> }}
<IconButton >
sx={{ <IconButton
pointerEvents: "none", sx={{
borderRadius: theme.shape.borderRadius, pointerEvents: "none",
border: `solid ${theme.shape.borderThick}px ${theme.palette.primary.lowContrast}`, borderRadius: theme.shape.borderRadius,
boxShadow: theme.shape.boxShadow, border: `solid ${theme.shape.borderThick}px ${theme.palette.primary.lowContrast}`,
}} boxShadow: theme.shape.boxShadow,
> }}
<CloudUploadIcon /> >
</IconButton> <CloudUploadIcon />
<Typography </IconButton>
component="h2" <Typography
color={theme.palette.primary.contrastTextTertiary} component="h2"
> color={theme.palette.primary.contrastTextTertiary}
<Typography >
component="span" <Typography
fontSize="inherit" component="span"
color="info" fontSize="inherit"
fontWeight={500} color="info"
> fontWeight={500}
{t('ClickUpload')} >
</Typography>{" "} {t("ClickUpload")}
or {t('DragandDrop')} </Typography>{" "}
</Typography> or {t("DragandDrop")}
<Typography </Typography>
component="p" <Typography
color={theme.palette.primary.contrastTextTertiary} component="p"
sx={{ opacity: 0.6 }} color={theme.palette.primary.contrastTextTertiary}
>({t('MaxSize')}: {Math.round(maxSize / 1024 / 1024)}MB) sx={{ opacity: 0.6 }}
</Typography> >
</Stack> ({t("MaxSize")}: {Math.round(maxSize / 1024 / 1024)}MB)
</Box> </Typography>
{(localError || progress.isLoading || progress.value !== 0) && ( </Stack>
<ProgressUpload </Box>
icon={<ImageIcon />} {(localError || progress.isLoading || progress.value !== 0) && (
label={file?.name || "Upload failed"} <ProgressUpload
size={file?.size} icon={<ImageIcon />}
progress={progress.value} label={file?.name || "Upload failed"}
onClick={() => { size={file?.size}
clearInterval(intervalRef.current); progress={progress.value}
setFile(null); onClick={() => {
setProgress({ value: 0, isLoading: false }); clearInterval(intervalRef.current);
setLocalError(null); setFile(null);
onChange(undefined); setProgress({ value: 0, isLoading: false });
}} setLocalError(null);
error={localError || error} onChange(undefined);
/> }}
)} error={localError || error}
<Typography />
component="p" )}
color={theme.palette.primary.contrastTextTertiary} <Typography
sx={{ opacity: 0.6 }} component="p"
> color={theme.palette.primary.contrastTextTertiary}
{t('SupportedFormats')}: {accept.join(", ").toUpperCase()} sx={{ opacity: 0.6 }}
</Typography> >
</> {t("SupportedFormats")}: {accept.join(", ").toUpperCase()}
)} </Typography>
</> </>
); )}
</>
);
}; };
ImageUpload.propTypes = { ImageUpload.propTypes = {
@@ -240,4 +239,4 @@ ImageUpload.propTypes = {
error: PropTypes.string, error: PropTypes.string,
}; };
export default ImageUpload; export default ImageUpload;
@@ -155,6 +155,7 @@ const Search = ({
renderOption={(props, option) => { renderOption={(props, option) => {
const { key, ...optionProps } = props; const { key, ...optionProps } = props;
const hasSecondaryLabel = secondaryLabel && option[secondaryLabel] !== undefined; const hasSecondaryLabel = secondaryLabel && option[secondaryLabel] !== undefined;
const port = option["port"];
return ( return (
<ListItem <ListItem
key={key} key={key}
@@ -169,7 +170,9 @@ const Search = ({
} }
> >
{option[filteredBy] + {option[filteredBy] +
(hasSecondaryLabel ? ` (${option[secondaryLabel]})` : "")} (hasSecondaryLabel
? ` (${option[secondaryLabel]}${port ? `: ${port}` : ""})`
: "")}
</ListItem> </ListItem>
); );
}} }}
+9 -1
View File
@@ -158,7 +158,15 @@ const StatusLabel = ({ status, text, customStyles }) => {
}; };
StatusLabel.propTypes = { StatusLabel.propTypes = {
status: PropTypes.oneOf(["up", "down", "paused", "pending", "cannot resolve", "published", "unpublished"]), status: PropTypes.oneOf([
"up",
"down",
"paused",
"pending",
"cannot resolve",
"published",
"unpublished",
]),
text: PropTypes.string, text: PropTypes.string,
customStyles: PropTypes.object, customStyles: PropTypes.object,
}; };
+15 -78
View File
@@ -21,107 +21,44 @@ const LanguageSelector = () => {
value={language} value={language}
onChange={handleChange} onChange={handleChange}
size="small" size="small"
sx={{ sx={{ minWidth: 80 }}
height: 28,
width: 64,
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
borderRadius: theme.shape.borderRadius,
fontSize: 10,
"& .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
},
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.primary.lowContrast,
},
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.primary.lowContrast,
},
"& .MuiSvgIcon-root": {
color: theme.palette.primary.contrastText,
width: 16,
height: 16,
right: 4,
top: "calc(50% - 8px)",
},
"& .MuiSelect-select": {
padding: "2px 20px 2px 8px",
display: "flex",
alignItems: "center",
fontSize: 10,
},
}}
MenuProps={{
PaperProps: {
sx: {
backgroundColor: theme.palette.primary.main,
borderRadius: theme.shape.borderRadius,
marginTop: 1,
width: 64,
"& .MuiMenuItem-root": {
padding: "2px 8px",
minHeight: 28,
fontSize: 10,
},
},
},
anchorOrigin: {
vertical: "bottom",
horizontal: "left",
},
transformOrigin: {
vertical: "top",
horizontal: "left",
},
}}
> >
{languages.map((lang) => { {languages.map((lang) => {
const flag = lang ? `fi fi-${lang}` : null; let parsedLang = lang === "en" ? "gb" : lang;
// Fix for Czech
if (parsedLang === "cs") {
parsedLang = "cz";
}
if (parsedLang.includes("-")) {
parsedLang = parsedLang.split("-")[1].toLowerCase();
console.log("parsedLang", parsedLang);
}
const flag = parsedLang ? `fi fi-${parsedLang}` : null;
return ( return (
<MenuItem <MenuItem
key={lang} key={lang}
value={lang} value={lang}
sx={{
color: theme.palette.primary.contrastText,
"&:hover": {
backgroundColor: theme.palette.primary.lowContrast,
},
"&.Mui-selected": {
backgroundColor: theme.palette.primary.lowContrast,
"&:hover": {
backgroundColor: theme.palette.primary.lowContrast,
},
},
}}
> >
<Stack <Stack
direction="row" direction="row"
spacing={2} spacing={theme.spacing(2)}
alignItems="center" alignItems="center"
ml={0.5}
> >
<Box <Box
component="span" component="span"
sx={{ sx={{
width: 16,
height: 12,
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
"& img": {
width: "100%",
height: "100%",
objectFit: "cover",
borderRadius: 0.5,
},
}} }}
> >
{flag && <span className={flag} />} {flag && <span className={flag} />}
</Box> </Box>
<Box <Box
component="span" component="span"
sx={{ textTransform: "uppercase", fontSize: 10 }} sx={{ textTransform: "uppercase" }}
> >
{lang} {lang}
</Box> </Box>
@@ -30,4 +30,4 @@
max-width: 1400px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
flex: 1; flex: 1;
} }
@@ -4,17 +4,11 @@ import PropTypes from "prop-types";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react"; import { useTheme } from "@emotion/react";
const CreateMonitorHeader = ({ const CreateMonitorHeader = ({ isAdmin, label, isLoading = true, path, bulkPath }) => {
isAdmin,
label,
isLoading = true,
path,
bulkPath,
}) => {
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
// Use the provided label or fall back to the translated default // Use the provided label or fall back to the translated default
if (!isAdmin) return null; if (!isAdmin) return null;
@@ -0,0 +1,120 @@
import Stack from "@mui/material/Stack";
import Status from "./status";
import Skeleton from "./skeleton";
import Button from "@mui/material/Button";
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
import PauseOutlinedIcon from "@mui/icons-material/PauseOutlined";
import PlayArrowOutlinedIcon from "@mui/icons-material/PlayArrowOutlined";
import EmailIcon from "@mui/icons-material/Email";
// Utils
import PropTypes from "prop-types";
import { useNavigate } from "react-router-dom";
import { useTheme } from "@mui/material/styles";
import { usePauseMonitor } from "../../Hooks/useMonitorControls";
import { useSendTestEmail } from "../../Hooks/useSendTestEmail";
import { useTranslation } from "react-i18next";
/**
* MonitorDetailsControlHeader component displays the control header for monitor details.
* It includes status display, pause/resume button, and a configure button for admins.
*
* @component
* @param {Object} props - Component props
* @param {string} props.path - The base path for navigation
* @param {boolean} [props.isLoading=false] - Flag indicating if the data is loading
* @param {boolean} [props.isAdmin=false] - Flag indicating if the user is an admin
* @param {Object} props.monitor - The monitor object containing details
* @param {Function} props.triggerUpdate - Function to trigger an update
* @returns {JSX.Element} The rendered component
*/
const MonitorDetailsControlHeader = ({
path,
isLoading = false,
isAdmin = false,
monitor,
triggerUpdate,
}) => {
const navigate = useNavigate();
const theme = useTheme();
const { t } = useTranslation();
const [pauseMonitor, isPausing, error] = usePauseMonitor({
monitorId: monitor?._id,
triggerUpdate,
});
const [isSending, emailError, sendTestEmail] = useSendTestEmail();
if (isLoading) {
return <Skeleton />;
}
return (
<Stack
direction="row"
justifyContent="space-between"
>
<Status monitor={monitor} />
<Stack
direction="row"
gap={theme.spacing(2)}
>
<Button
variant="contained"
color="secondary"
loading={isSending}
startIcon={<EmailIcon />}
onClick={() => {
sendTestEmail();
}}
>
{t("sendTestEmail")}
</Button>
<Button
variant="contained"
color="secondary"
onClick={(e) => {
navigate(`/incidents/${monitor?._id}`);
}}
>
{t("menu.incidents")}
</Button>
<Button
variant="contained"
color="secondary"
loading={isPausing}
startIcon={
monitor?.isActive ? <PauseOutlinedIcon /> : <PlayArrowOutlinedIcon />
}
onClick={() => {
pauseMonitor();
}}
>
{monitor?.isActive ? "Pause" : "Resume"}
</Button>
{isAdmin && (
<Button
variant="contained"
color="secondary"
startIcon={<SettingsOutlinedIcon />}
onClick={() => navigate(`/${path}/configure/${monitor._id}`)}
>
Configure
</Button>
)}
</Stack>
</Stack>
);
};
MonitorDetailsControlHeader.propTypes = {
path: PropTypes.string,
isLoading: PropTypes.bool,
isAdmin: PropTypes.bool,
monitor: PropTypes.object,
triggerUpdate: PropTypes.func,
};
export default MonitorDetailsControlHeader;
@@ -0,0 +1,23 @@
import { Stack, Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Stack
direction="row"
justifyContent="space-between"
>
<Skeleton
height={40}
variant="rounded"
width="15%"
/>
<Skeleton
height={40}
variant="rounded"
width="15%"
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -0,0 +1,56 @@
// Components
import Stack from "@mui/material/Stack";
import PulseDot from "../../Components/Animated/PulseDot";
import Typography from "@mui/material/Typography";
import Dot from "../../Components/Dot";
// Utils
import { formatDurationRounded } from "../../Utils/timeUtils";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils";
/**
* Status component displays the status information of a monitor.
* It includes the monitor's name, URL, and check interval.
*
* @component
* @param {Object} props - Component props
* @param {Object} props.monitor - The monitor object containing details
* @param {string} props.monitor.name - The name of the monitor
* @param {string} props.monitor.url - The URL of the monitor
* @param {number} props.monitor.interval - The interval at which the monitor checks
* @returns {JSX.Element} The rendered component
*/
const Status = ({ monitor }) => {
const theme = useTheme();
const { statusColor, determineState } = useUtils();
return (
<Stack>
<Typography variant="h1">{monitor?.name}</Typography>
<Stack
direction="row"
alignItems={"center"}
gap={theme.spacing(4)}
>
<PulseDot color={statusColor[determineState(monitor)]} />
<Typography
variant="h2"
style={{ fontFamily: "monospace", fontWeight: "bolder" }}
>
{monitor?.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Dot />
<Typography>
Checking every {formatDurationRounded(monitor?.interval)}.
</Typography>
</Stack>
</Stack>
);
};
Status.propTypes = {
monitor: PropTypes.object,
};
export default Status;
@@ -28,8 +28,9 @@ const MonitorStatusHeader = ({ path, isLoading = false, isAdmin, monitor }) => {
gap={theme.spacing(4)} gap={theme.spacing(4)}
> >
<PulseDot color={statusColor[determineState(monitor)]} /> <PulseDot color={statusColor[determineState(monitor)]} />
<Typography variant="h2" <Typography
style={{fontFamily : "monospace" , fontWeight : 'bolder'}} variant="h2"
style={{ fontFamily: "monospace", fontWeight: "bolder" }}
> >
{monitor?.url?.replace(/^https?:\/\//, "") || "..."} {monitor?.url?.replace(/^https?:\/\//, "") || "..."}
</Typography> </Typography>
@@ -1,109 +1,115 @@
import React from "react"; import React from "react";
import { import { Typography, Box, Button, CircularProgress } from "@mui/material";
Typography,
Box,
Button,
CircularProgress
} from "@mui/material";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react"; import { useTheme } from "@emotion/react";
import TextInput from "../../../Components/Inputs/TextInput"; import TextInput from "../../../Components/Inputs/TextInput";
import Checkbox from "../../../Components/Inputs/Checkbox"; import Checkbox from "../../../Components/Inputs/Checkbox";
const TabComponent = ({ const TabComponent = ({
type, type,
integrations, integrations,
handleIntegrationChange, handleIntegrationChange,
handleInputChange, handleInputChange,
handleTestNotification, handleTestNotification,
isLoading isLoading,
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
// Helper to get the field state key (e.g., slackWebhook, telegramToken) // Helper to get the field state key (e.g., slackWebhook, telegramToken)
const getFieldKey = (typeId, fieldId) => { const getFieldKey = (typeId, fieldId) => {
return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`; return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`;
}; };
// Check if all fields have values to enable test button // Check if all fields have values to enable test button
const areAllFieldsFilled = () => { const areAllFieldsFilled = () => {
return type.fields.every(field => { return type.fields.every((field) => {
const fieldKey = getFieldKey(type.id, field.id); const fieldKey = getFieldKey(type.id, field.id);
return integrations[fieldKey]; return integrations[fieldKey];
}); });
}; };
return ( return (
<> <>
<Typography variant="subtitle1" component="h4" sx={{ <Typography
fontWeight: 'bold', variant="subtitle1"
color: theme.palette.primary.contrastTextSecondary component="h4"
}}> sx={{
{type.label} fontWeight: "bold",
</Typography> color: theme.palette.primary.contrastTextSecondary,
}}
<Typography sx={{ >
mt: theme.spacing(0.5), {type.label}
mb: theme.spacing(1.5), </Typography>
color: theme.palette.primary.contrastTextTertiary
}}> <Typography
{type.description} sx={{
</Typography> mt: theme.spacing(0.5),
mb: theme.spacing(1.5),
<Box sx={{ pl: theme.spacing(1.5) }}> color: theme.palette.primary.contrastTextTertiary,
<Checkbox }}
id={`enable-${type.id}`} >
label={t('notifications.enableNotifications', { platform: type.label })} {type.description}
isChecked={integrations[type.id]} </Typography>
onChange={(e) => handleIntegrationChange(type.id, e.target.checked)}
disabled={isLoading} <Box sx={{ pl: theme.spacing(1.5) }}>
/> <Checkbox
</Box> id={`enable-${type.id}`}
label={t("notifications.enableNotifications", { platform: type.label })}
{type.fields.map(field => { isChecked={integrations[type.id]}
const fieldKey = getFieldKey(type.id, field.id); onChange={(e) => handleIntegrationChange(type.id, e.target.checked)}
disabled={isLoading}
return ( />
<Box key={field.id} sx={{ mt: theme.spacing(1) }}> </Box>
<Typography sx={{
mb: theme.spacing(2), {type.fields.map((field) => {
fontWeight: 'bold', const fieldKey = getFieldKey(type.id, field.id);
color: theme.palette.primary.contrastTextSecondary
}}> return (
{field.label} <Box
</Typography> key={field.id}
sx={{ mt: theme.spacing(1) }}
<TextInput >
id={`${type.id}-${field.id}`} <Typography
type={field.type} sx={{
placeholder={field.placeholder} mb: theme.spacing(2),
value={integrations[fieldKey]} fontWeight: "bold",
onChange={(e) => handleInputChange(fieldKey, e.target.value)} color: theme.palette.primary.contrastTextSecondary,
disabled={!integrations[type.id] || isLoading} }}
/> >
</Box> {field.label}
); </Typography>
})}
<TextInput
<Box sx={{ mt: theme.spacing(1) }}> id={`${type.id}-${field.id}`}
<Button type={field.type}
variant="text" placeholder={field.placeholder}
color="info" value={integrations[fieldKey]}
onClick={() => handleTestNotification(type.id)} onChange={(e) => handleInputChange(fieldKey, e.target.value)}
disabled={!integrations[type.id] || !areAllFieldsFilled() || isLoading} disabled={!integrations[type.id] || isLoading}
> />
{isLoading ? ( </Box>
<CircularProgress );
size={theme.spacing(8)} })}
sx={{ mr: theme.spacing(1), color: theme.palette.accent.main}}
/> <Box sx={{ mt: theme.spacing(1) }}>
) : null} <Button
{t('notifications.testNotification')} variant="text"
</Button> color="info"
</Box> onClick={() => handleTestNotification(type.id)}
</> disabled={!integrations[type.id] || !areAllFieldsFilled() || isLoading}
); >
{isLoading ? (
<CircularProgress
size={theme.spacing(8)}
sx={{ mr: theme.spacing(1), color: theme.palette.accent.main }}
/>
) : null}
{t("notifications.testNotification")}
</Button>
</Box>
</>
);
}; };
export default TabComponent; export default TabComponent;
@@ -5,7 +5,7 @@ import { useTheme } from "@emotion/react";
/** /**
* TabPanel component that displays content for the selected tab. * TabPanel component that displays content for the selected tab.
* *
* @component * @component
* @param {Object} props - The component props. * @param {Object} props - The component props.
* @param {React.ReactNode} props.children - The content to be displayed when this tab panel is selected. * @param {React.ReactNode} props.children - The content to be displayed when this tab panel is selected.
@@ -15,32 +15,27 @@ import { useTheme } from "@emotion/react";
* @returns {React.ReactElement|null} The rendered tab panel or null if not selected. * @returns {React.ReactElement|null} The rendered tab panel or null if not selected.
*/ */
function TabPanel({ children, value, index, ...other }) { function TabPanel({ children, value, index, ...other }) {
const theme = useTheme(); const theme = useTheme();
return ( return (
<div <div
role="tabpanel" role="tabpanel"
hidden={value !== index} hidden={value !== index}
id={`notification-tabpanel-${index}`} id={`notification-tabpanel-${index}`}
aria-labelledby={`notification-tab-${index}`} aria-labelledby={`notification-tab-${index}`}
{...other} {...other}
> >
{value === index && ( {value === index && <Box sx={{ pt: theme.spacing(3) }}>{children}</Box>}
<Box sx={{ pt: theme.spacing(3) }}> </div>
{children} );
</Box>
)}
</div>
);
} }
TabPanel.propTypes = { TabPanel.propTypes = {
children: PropTypes.node,
children: PropTypes.node, index: PropTypes.number.isRequired,
index: PropTypes.number.isRequired, value: PropTypes.number.isRequired,
value: PropTypes.number.isRequired
}; };
export default TabPanel; export default TabPanel;
@@ -1,128 +1,129 @@
import { useState } from 'react'; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { networkService } from '../../../Utils/NetworkService'; import { networkService } from "../../../Utils/NetworkService";
import { createToast } from '../../../Utils/toastUtils'; import { createToast } from "../../../Utils/toastUtils";
// Define constants for notification types to avoid magic values // Define constants for notification types to avoid magic values
const NOTIFICATION_TYPES = { const NOTIFICATION_TYPES = {
SLACK: 'slack', SLACK: "slack",
DISCORD: 'discord', DISCORD: "discord",
TELEGRAM: 'telegram', TELEGRAM: "telegram",
WEBHOOK: 'webhook' WEBHOOK: "webhook",
}; };
// Define constants for field IDs // Define constants for field IDs
const FIELD_IDS = { const FIELD_IDS = {
WEBHOOK: 'webhook', WEBHOOK: "webhook",
TOKEN: 'token', TOKEN: "token",
CHAT_ID: 'chatId', CHAT_ID: "chatId",
URL: 'url' URL: "url",
}; };
/** /**
* Custom hook for notification-related operations * Custom hook for notification-related operations
*/ */
const useNotifications = () => { const useNotifications = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(undefined); const [error, setError] = useState(undefined);
const { t } = useTranslation(); const { t } = useTranslation();
/** /**
* Send a test notification * Send a test notification
* @param {string} type - The notification type (slack, discord, telegram, webhook) * @param {string} type - The notification type (slack, discord, telegram, webhook)
* @param {object} config - Configuration object with necessary params * @param {object} config - Configuration object with necessary params
*/ */
const sendTestNotification = async (type, config) => { const sendTestNotification = async (type, config) => {
setLoading(true); setLoading(true);
setError(undefined); setError(undefined);
// Validation based on notification type
let payload = { platform: type };
let isValid = true;
let errorMessage = '';
switch(type) {
case NOTIFICATION_TYPES.SLACK:
payload.webhookUrl = config.webhook;
if (typeof payload.webhookUrl === 'undefined' || payload.webhookUrl === '') {
isValid = false;
errorMessage = t('notifications.slack.webhookRequired');
}
break;
case NOTIFICATION_TYPES.DISCORD:
payload.webhookUrl = config.webhook;
if (typeof payload.webhookUrl === 'undefined' || payload.webhookUrl === '') {
isValid = false;
errorMessage = t('notifications.discord.webhookRequired');
}
break;
case NOTIFICATION_TYPES.TELEGRAM:
payload.botToken = config.token;
payload.chatId = config.chatId;
if (typeof payload.botToken === 'undefined' || payload.botToken === '' ||
typeof payload.chatId === 'undefined' || payload.chatId === '') {
isValid = false;
errorMessage = t('notifications.telegram.fieldsRequired');
}
break;
case NOTIFICATION_TYPES.WEBHOOK:
payload.webhookUrl = config.url;
payload.platform = NOTIFICATION_TYPES.SLACK;
if (typeof payload.webhookUrl === 'undefined' || payload.webhookUrl === '') {
isValid = false;
errorMessage = t('notifications.webhook.urlRequired');
}
break;
default:
isValid = false;
errorMessage = t('notifications.unsupportedType');
}
// If validation fails, show error and return // Validation based on notification type
if (isValid === false) { let payload = { platform: type };
createToast({ let isValid = true;
body: errorMessage, let errorMessage = "";
variant: "error"
});
setLoading(false);
return;
}
try { switch (type) {
const response = await networkService.testNotification({ case NOTIFICATION_TYPES.SLACK:
platform: type, payload.webhookUrl = config.webhook;
payload: payload if (typeof payload.webhookUrl === "undefined" || payload.webhookUrl === "") {
}); isValid = false;
errorMessage = t("notifications.slack.webhookRequired");
if (response.data.success === true) { }
createToast({ break;
body: t('notifications.testSuccess'),
variant: "info"
});
} else {
throw new Error(response.data.msg || t('notifications.testFailed'));
}
} catch (error) {
const errorMsg = error.response?.data?.msg || error.message || t('notifications.networkError');
createToast({
body: `${t('notifications.testFailed')}: ${errorMsg}`,
variant: "error"
});
setError(errorMsg);
} finally {
setLoading(false);
}
};
return [ case NOTIFICATION_TYPES.DISCORD:
loading, payload.webhookUrl = config.webhook;
error, if (typeof payload.webhookUrl === "undefined" || payload.webhookUrl === "") {
sendTestNotification isValid = false;
]; errorMessage = t("notifications.discord.webhookRequired");
}
break;
case NOTIFICATION_TYPES.TELEGRAM:
payload.botToken = config.token;
payload.chatId = config.chatId;
if (
typeof payload.botToken === "undefined" ||
payload.botToken === "" ||
typeof payload.chatId === "undefined" ||
payload.chatId === ""
) {
isValid = false;
errorMessage = t("notifications.telegram.fieldsRequired");
}
break;
case NOTIFICATION_TYPES.WEBHOOK:
payload.webhookUrl = config.url;
payload.platform = NOTIFICATION_TYPES.SLACK;
if (typeof payload.webhookUrl === "undefined" || payload.webhookUrl === "") {
isValid = false;
errorMessage = t("notifications.webhook.urlRequired");
}
break;
default:
isValid = false;
errorMessage = t("notifications.unsupportedType");
}
// If validation fails, show error and return
if (isValid === false) {
createToast({
body: errorMessage,
variant: "error",
});
setLoading(false);
return;
}
try {
const response = await networkService.testNotification({
platform: type,
payload: payload,
});
if (response.data.success === true) {
createToast({
body: t("notifications.testSuccess"),
variant: "info",
});
} else {
throw new Error(response.data.msg || t("notifications.testFailed"));
}
} catch (error) {
const errorMsg =
error.response?.data?.msg || error.message || t("notifications.networkError");
createToast({
body: `${t("notifications.testFailed")}: ${errorMsg}`,
variant: "error",
});
setError(errorMsg);
} finally {
setLoading(false);
}
};
return [loading, error, sendTestNotification];
}; };
export default useNotifications; export default useNotifications;
+1 -1
View File
@@ -93,7 +93,7 @@ const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) =>
<Typography <Typography
component="h2" component="h2"
mb={theme.spacing(1.5)} mb={theme.spacing(1.5)}
sx={{ wordBreak: 'break-all' }} sx={{ wordBreak: "break-all" }}
> >
{error ? error : label} {error ? error : label}
</Typography> </Typography>
+14 -10
View File
@@ -121,7 +121,7 @@ function Sidebar() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const authState = useSelector((state) => state.auth); const authState = useSelector((state) => state.auth);
const menu = getMenu(t); const menu = getMenu(t);
const otherMenuItems = getOtherMenuItems(t); const otherMenuItems = getOtherMenuItems(t);
const accountMenuItems = getAccountMenuItems(t); const accountMenuItems = getAccountMenuItems(t);
@@ -147,7 +147,7 @@ function Sidebar() {
} else { } else {
setSidebarReady(false); setSidebarReady(false);
} }
}, [collapsed]); }, [collapsed]);
const renderAccountMenuItems = () => { const renderAccountMenuItems = () => {
let filteredAccountMenuItems = [...accountMenuItems]; let filteredAccountMenuItems = [...accountMenuItems];
@@ -170,8 +170,8 @@ function Sidebar() {
<MenuItem <MenuItem
key={item.name} key={item.name}
onClick={() => { onClick={() => {
closePopup() closePopup();
navigate(item.path) navigate(item.path);
}} }}
sx={{ sx={{
gap: theme.spacing(2), gap: theme.spacing(2),
@@ -231,7 +231,7 @@ function Sidebar() {
*/ */
sx={{ sx={{
position: "relative", position: "relative",
borderRight: `1px solid ${theme.palette.primary.lowContrast}`, borderRight: `1px solid ${theme.palette.primary.lowContrast}`,
borderColor: theme.palette.primary.lowContrast, borderColor: theme.palette.primary.lowContrast,
borderRadius: 0, borderRadius: 0,
backgroundColor: theme.palette.primary.main, backgroundColor: theme.palette.primary.main,
@@ -732,11 +732,15 @@ function Sidebar() {
{authState.user?.firstName} {authState.user?.lastName} {authState.user?.firstName} {authState.user?.lastName}
</Typography> </Typography>
<Typography sx={{ textTransform: "capitalize" }}> <Typography sx={{ textTransform: "capitalize" }}>
{authState.user?.role?.includes("superadmin") ? t("roles.superAdmin") : {authState.user?.role?.includes("superadmin")
authState.user?.role?.includes("admin") ? t("roles.admin") : ? t("roles.superAdmin")
authState.user?.role?.includes("user") ? t("roles.teamMember") : : authState.user?.role?.includes("admin")
authState.user?.role?.includes("demo") ? t("roles.demoUser") : ? t("roles.admin")
authState.user?.role} : authState.user?.role?.includes("user")
? t("roles.teamMember")
: authState.user?.role?.includes("demo")
? t("roles.demoUser")
: authState.user?.role}
</Typography> </Typography>
</Box> </Box>
<Stack <Stack
+98 -91
View File
@@ -1,101 +1,108 @@
import React from 'react'; import React from "react";
import { Typography, IconButton, Stack, Box } from '@mui/material'; import { Typography, IconButton, Stack, Box } from "@mui/material";
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from "@mui/icons-material/Close";
import { useTheme } from '@emotion/react'; import { useTheme } from "@emotion/react";
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from "react-redux";
import { useTranslation } from 'react-i18next'; import { useTranslation } from "react-i18next";
import { setStarPromptOpen } from '../../Features/UI/uiSlice'; import { setStarPromptOpen } from "../../Features/UI/uiSlice";
const StarPrompt = ({ const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" }) => {
repoUrl = 'https://github.com/bluewave-labs/checkmate' const theme = useTheme();
}) => { const dispatch = useDispatch();
const theme = useTheme(); const { t } = useTranslation();
const dispatch = useDispatch(); const isOpen = useSelector((state) => state.ui?.starPromptOpen ?? true);
const { t } = useTranslation(); const mode = useSelector((state) => state.ui.mode);
const isOpen = useSelector((state) => state.ui?.starPromptOpen ?? true);
const mode = useSelector((state) => state.ui.mode);
const handleClose = () => { const handleClose = () => {
dispatch(setStarPromptOpen(false)); dispatch(setStarPromptOpen(false));
}; };
const handleStarClick = () => { const handleStarClick = () => {
window.open(repoUrl, '_blank'); window.open(repoUrl, "_blank");
}; };
if (!isOpen) return null; if (!isOpen) return null;
return ( return (
<Stack <Stack
direction="column" direction="column"
sx={{ sx={{
width: '100%', width: "100%",
padding: `${theme.spacing(6)} ${theme.spacing(6)}`, padding: `${theme.spacing(6)} ${theme.spacing(6)}`,
borderTop: `1px solid ${theme.palette.primary.lowContrast}`, borderTop: `1px solid ${theme.palette.primary.lowContrast}`,
borderBottom: `1px solid ${theme.palette.primary.lowContrast}`, borderBottom: `1px solid ${theme.palette.primary.lowContrast}`,
borderRadius: 0, borderRadius: 0,
gap: theme.spacing(1.5), gap: theme.spacing(1.5),
backgroundColor: theme.palette.primary.main, backgroundColor: theme.palette.primary.main,
}} }}
> >
<Stack direction="row" justifyContent="space-between" alignItems="center" width="100%" pl={theme.spacing(4)}> <Stack
<Typography direction="row"
variant="subtitle2" justifyContent="space-between"
sx={{ alignItems="center"
color: mode === 'dark' ? theme.palette.primary.contrastText : theme.palette.text.primary, width="100%"
mt: theme.spacing(3) pl={theme.spacing(4)}
}} >
> <Typography
{t('starPromptTitle')} variant="subtitle2"
</Typography> sx={{
<IconButton color:
onClick={handleClose} mode === "dark"
size="small" ? theme.palette.primary.contrastText
sx={{ : theme.palette.text.primary,
color: theme.palette.text.primary, mt: theme.spacing(3),
padding: 0, }}
marginTop: theme.spacing(-5), >
'&:hover': { {t("starPromptTitle")}
backgroundColor: 'transparent', </Typography>
opacity: 0.8 <IconButton
}, onClick={handleClose}
}} size="small"
> sx={{
<CloseIcon sx={{ fontSize: '1.25rem' }} /> color: theme.palette.text.primary,
</IconButton> padding: 0,
</Stack> marginTop: theme.spacing(-5),
"&:hover": {
backgroundColor: "transparent",
opacity: 0.8,
},
}}
>
<CloseIcon sx={{ fontSize: "1.25rem" }} />
</IconButton>
</Stack>
<Typography <Typography
variant="body1" variant="body1"
sx={{ sx={{
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
fontSize: '0.938rem', fontSize: "0.938rem",
lineHeight: 1.5, lineHeight: 1.5,
mb: 1, mb: 1,
px: theme.spacing(4) px: theme.spacing(4),
}} }}
> >
{t('starPromptDescription')} {t("starPromptDescription")}
</Typography> </Typography>
<Box <Box
component="img" component="img"
src={`https://img.shields.io/github/stars/bluewave-labs/checkmate?label=checkmate&style=social${mode === 'dark' ? '&color=white' : ''}`} src={`https://img.shields.io/github/stars/bluewave-labs/checkmate?label=checkmate&style=social${mode === "dark" ? "&color=white" : ""}`}
alt="GitHub stars" alt="GitHub stars"
onClick={handleStarClick} onClick={handleStarClick}
sx={{ sx={{
cursor: 'pointer', cursor: "pointer",
transform: 'scale(0.65)', transform: "scale(0.65)",
transformOrigin: 'left center', transformOrigin: "left center",
'&:hover': { "&:hover": {
opacity: 0.8 opacity: 0.8,
}, },
pl: theme.spacing(4), pl: theme.spacing(4),
filter: mode === 'dark' ? 'invert(1)' : 'none' filter: mode === "dark" ? "invert(1)" : "none",
}} }}
/> />
</Stack> </Stack>
); );
}; };
export default StarPrompt; export default StarPrompt;
+1 -1
View File
@@ -80,7 +80,7 @@ const StatBox = ({
}; };
const responsiveWidths = { const responsiveWidths = {
default: `calc(25% - (3 * ${theme.spacing(8)} / 4))`, default: `calc(25% - (3 * ${theme.spacing(8)} / 4))`,
md: `calc(50% - (1 * ${theme.spacing(8)} / 2))`, md: `calc(50% - (1 * ${theme.spacing(8)} / 2))`,
}; };
+5 -1
View File
@@ -20,7 +20,11 @@ const CustomTabList = ({ value, onChange, children, ...props }) => {
"& .MuiTabs-root": { height: "fit-content", minHeight: "0" }, "& .MuiTabs-root": { height: "fit-content", minHeight: "0" },
}} }}
> >
<TabList value={value} onChange={onChange} {...props}> <TabList
value={value}
onChange={onChange}
{...props}
>
{children} {children}
</TabList> </TabList>
</Box> </Box>
@@ -94,7 +94,10 @@ const PasswordPanel = () => {
const action = await dispatch(update({ localData })); const action = await dispatch(update({ localData }));
if (action.payload.success) { if (action.payload.success) {
createToast({ createToast({
body: t("passwordPanel.passwordChangedSuccess", "Your password was changed successfully."), body: t(
"passwordPanel.passwordChangedSuccess",
"Your password was changed successfully."
),
}); });
setLocalData({ setLocalData({
password: "", password: "",
@@ -104,7 +107,10 @@ const PasswordPanel = () => {
} else { } else {
// TODO: Check for other errors? // TODO: Check for other errors?
createToast({ createToast({
body: t("passwordPanel.passwordInputIncorrect", "Your password input was incorrect."), body: t(
"passwordPanel.passwordInputIncorrect",
"Your password input was incorrect."
),
}); });
setErrors({ password: "*" + action.payload.msg + "." }); setErrors({ password: "*" + action.payload.msg + "." });
} }
@@ -153,7 +159,10 @@ const PasswordPanel = () => {
<TextInput <TextInput
type="password" type="password"
id="edit-current-password" id="edit-current-password"
placeholder={t("passwordPanel.enterCurrentPassword", "Enter your current password")} placeholder={t(
"passwordPanel.enterCurrentPassword",
"Enter your current password"
)}
autoComplete="current-password" autoComplete="current-password"
value={localData.password} value={localData.password}
onChange={handleChange} onChange={handleChange}
@@ -219,7 +228,10 @@ const PasswordPanel = () => {
<Box sx={{ maxWidth: "70ch" }}> <Box sx={{ maxWidth: "70ch" }}>
<Alert <Alert
variant="warning" variant="warning"
body={t("passwordPanel.passwordRequirements", "New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character.")} body={t(
"passwordPanel.passwordRequirements",
"New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character."
)}
/> />
</Box> </Box>
)} )}
@@ -85,13 +85,13 @@ const ProfilePanel = () => {
// Resets picture-related states and clears interval // Resets picture-related states and clears interval
const removePicture = () => { const removePicture = () => {
errors["picture"] && clearError("picture"); errors["picture"] && clearError("picture");
setFile(undefined); setFile(undefined);
setLocalData((prev) => ({ setLocalData((prev) => ({
...prev, ...prev,
file: undefined, file: undefined,
deleteProfileImage: true, deleteProfileImage: true,
})); }));
}; };
// Opens the picture update modal // Opens the picture update modal
const openPictureModal = () => { const openPictureModal = () => {
@@ -102,9 +102,9 @@ const ProfilePanel = () => {
// Closes the picture update modal and resets related states // Closes the picture update modal and resets related states
const closePictureModal = () => { const closePictureModal = () => {
if (errors["picture"]) clearError("picture"); if (errors["picture"]) clearError("picture");
setFile(undefined); setFile(undefined);
setIsOpen(""); setIsOpen("");
}; };
// Updates profile image displayed on UI // Updates profile image displayed on UI
const handleUpdatePicture = () => { const handleUpdatePicture = () => {
@@ -116,24 +116,23 @@ const ProfilePanel = () => {
setIsOpen(""); setIsOpen("");
if (errors["unchanged"]) clearError("unchanged"); if (errors["unchanged"]) clearError("unchanged");
}; };
// Handles form submission to update user profile // Handles form submission to update user profile
const handleSaveProfile = async (event) => { const handleSaveProfile = async (event) => {
event.preventDefault(); event.preventDefault();
const nameChanged = const nameChanged =
localData.firstName !== user.firstName || localData.firstName !== user.firstName || localData.lastName !== user.lastName;
localData.lastName !== user.lastName;
const avatarChanged = const avatarChanged =
localData.deleteProfileImage === true || localData.deleteProfileImage === true ||
(localData.file && localData.file !== `data:image/png;base64,${user.avatarImage}`); (localData.file && localData.file !== `data:image/png;base64,${user.avatarImage}`);
if (!nameChanged && !avatarChanged) { if (!nameChanged && !avatarChanged) {
createToast({ createToast({
body: "Unable to update profile — no changes detected.", body: "Unable to update profile — no changes detected.",
}); });
setErrors({ unchanged: "unable to update profile" }); setErrors({ unchanged: "unable to update profile" });
return; return;
} }
const action = await dispatch(update({ localData })); const action = await dispatch(update({ localData }));
@@ -205,7 +204,7 @@ const ProfilePanel = () => {
> >
{/* This 0.9 is a bit magic numbering, refactor */} {/* This 0.9 is a bit magic numbering, refactor */}
<Box flex={0.9}> <Box flex={0.9}>
<Typography component="h1">{t('FirstName')}</Typography> <Typography component="h1">{t("FirstName")}</Typography>
</Box> </Box>
<TextInput <TextInput
id="edit-first-name" id="edit-first-name"
@@ -223,7 +222,7 @@ const ProfilePanel = () => {
gap={SPACING_GAP} gap={SPACING_GAP}
> >
<Box flex={0.9}> <Box flex={0.9}>
<Typography component="h1">{t('LastName')}</Typography> <Typography component="h1">{t("LastName")}</Typography>
</Box> </Box>
<TextInput <TextInput
id="edit-last-name" id="edit-last-name"
@@ -241,12 +240,12 @@ const ProfilePanel = () => {
gap={SPACING_GAP} gap={SPACING_GAP}
> >
<Stack flex={0.9}> <Stack flex={0.9}>
<Typography component="h1">{t('email')}</Typography> <Typography component="h1">{t("email")}</Typography>
<Typography <Typography
component="p" component="p"
sx={{ opacity: 0.6 }} sx={{ opacity: 0.6 }}
> >
{t('EmailDescriptionText')} {t("EmailDescriptionText")}
</Typography> </Typography>
</Stack> </Stack>
<TextInput <TextInput
@@ -264,12 +263,12 @@ const ProfilePanel = () => {
gap={SPACING_GAP} gap={SPACING_GAP}
> >
<Stack flex={0.9}> <Stack flex={0.9}>
<Typography component="h1">{t('YourPhoto')}</Typography> <Typography component="h1">{t("YourPhoto")}</Typography>
<Typography <Typography
component="p" component="p"
sx={{ opacity: 0.6 }} sx={{ opacity: 0.6 }}
> >
{t('PhotoDescriptionText')} {t("PhotoDescriptionText")}
</Typography> </Typography>
</Stack> </Stack>
<Stack <Stack
@@ -293,14 +292,14 @@ const ProfilePanel = () => {
color="error" color="error"
onClick={handleDeletePicture} onClick={handleDeletePicture}
> >
{t('delete')} {t("delete")}
</Button> </Button>
<Button <Button
variant="contained" // CAIO_REVIEW variant="contained" // CAIO_REVIEW
color="accent" color="accent"
onClick={openPictureModal} onClick={openPictureModal}
> >
{t('update')} {t("update")}
</Button> </Button>
</Stack> </Stack>
</Stack> </Stack>
@@ -325,7 +324,7 @@ const ProfilePanel = () => {
disabled={Object.keys(errors).length !== 0 && !errors?.picture && true} disabled={Object.keys(errors).length !== 0 && !errors?.picture && true}
sx={{ px: theme.spacing(12) }} sx={{ px: theme.spacing(12) }}
> >
{t('save')} {t("save")}
</Button> </Button>
</Box> </Box>
</Stack> </Stack>
@@ -344,12 +343,12 @@ const ProfilePanel = () => {
spellCheck="false" spellCheck="false"
> >
<Box mb={theme.spacing(6)}> <Box mb={theme.spacing(6)}>
<Typography component="h1">{t('DeleteAccountTitle')}</Typography> <Typography component="h1">{t("DeleteAccountTitle")}</Typography>
<Typography <Typography
component="p" component="p"
sx={{ opacity: 0.6 }} sx={{ opacity: 0.6 }}
> >
{t('DeleteDescriptionText')} {t("DeleteDescriptionText")}
</Typography> </Typography>
</Box> </Box>
<Button <Button
@@ -357,17 +356,17 @@ const ProfilePanel = () => {
color="error" color="error"
onClick={() => setIsOpen("delete")} onClick={() => setIsOpen("delete")}
> >
{t('DeleteAccountButton')} {t("DeleteAccountButton")}
</Button> </Button>
</Box> </Box>
)} )}
<Dialog <Dialog
open={isModalOpen("delete")} open={isModalOpen("delete")}
theme={theme} theme={theme}
title={t('DeleteWarningTitle')} title={t("DeleteWarningTitle")}
description={t('DeleteAccountWarning')} description={t("DeleteAccountWarning")}
onCancel={() => setIsOpen("")} onCancel={() => setIsOpen("")}
confirmationButtonLabel={t('DeleteAccountButton')} confirmationButtonLabel={t("DeleteAccountButton")}
onConfirm={handleDeleteAccount} onConfirm={handleDeleteAccount}
isLoading={isLoading} isLoading={isLoading}
/> />
@@ -389,13 +388,13 @@ const ProfilePanel = () => {
: user?.avatarImage : user?.avatarImage
? `data:image/png;base64,${user.avatarImage}` ? `data:image/png;base64,${user.avatarImage}`
: "" : ""
} }
onChange={(newFile) => { onChange={(newFile) => {
if (newFile) { if (newFile) {
setFile(newFile); setFile(newFile);
clearError("unchanged"); clearError("unchanged");
} }
}} }}
previewIsRound previewIsRound
maxSize={3 * 1024 * 1024} maxSize={3 * 1024 * 1024}
/> />
@@ -410,7 +409,7 @@ const ProfilePanel = () => {
color="info" color="info"
onClick={removePicture} onClick={removePicture}
> >
{t('remove')} {t("remove")}
</Button> </Button>
<Button <Button
variant="contained" variant="contained"
@@ -418,7 +417,7 @@ const ProfilePanel = () => {
onClick={handleUpdatePicture} onClick={handleUpdatePicture}
disabled={!!errors.picture || !file?.src} disabled={!!errors.picture || !file?.src}
> >
{t('update')} {t("update")}
</Button> </Button>
</Stack> </Stack>
</GenericDialog> </GenericDialog>
@@ -48,7 +48,8 @@ const TeamPanel = () => {
{row.firstName + " " + row.lastName} {row.firstName + " " + row.lastName}
</Typography> </Typography>
<Typography> <Typography>
{t("teamPanel.table.created")} {new Date(row.createdAt).toLocaleDateString()} {t("teamPanel.table.created")}{" "}
{new Date(row.createdAt).toLocaleDateString()}
</Typography> </Typography>
</Stack> </Stack>
); );
+22 -20
View File
@@ -1,10 +1,10 @@
.wallet-adapter-dropdown { .wallet-adapter-dropdown {
width: 100%; width: 100%;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: var(--env-var-spacing-1); gap: var(--env-var-spacing-1);
} }
.wallet-adapter-button { .wallet-adapter-button {
@@ -15,37 +15,39 @@
height: var(--env-var-height-2) !important; height: var(--env-var-height-2) !important;
font-size: var(--env-var-font-size-medium-plus) !important; font-size: var(--env-var-font-size-medium-plus) !important;
font-weight: 500 !important; font-weight: 500 !important;
margin: 0; margin: 0;
padding: calc((var(--env-var-height-2) - var(--env-var-font-size-medium-plus) * 1.2) / 2) var(--env-var-spacing-1); padding: calc(
(var(--env-var-height-2) - var(--env-var-font-size-medium-plus) * 1.2) / 2
)
var(--env-var-spacing-1);
white-space: nowrap; white-space: nowrap;
} }
.wallet-adapter-modal-title { .wallet-adapter-modal-title {
font-size: var(--font-size-h5) !important; font-size: var(--font-size-h5) !important;
} }
/* Separator styling */ /* Separator styling */
.wallet-adapter-modal-divider { .wallet-adapter-modal-divider {
background-color: var(--border-color) !important; background-color: var(--border-color) !important;
margin: var(--spacing-md) 0 !important; margin: var(--spacing-md) 0 !important;
} }
/* Responsive fixes */ /* Responsive fixes */
@media (max-width: 1200px) { @media (max-width: 1200px) {
.wallet-adapter-button { .wallet-adapter-button {
font-size: var(--env-var-font-size-medium) !important; font-size: var(--env-var-font-size-medium) !important;
padding: calc((var(--env-var-height-2) - var(--env-var-font-size-medium) * 1.2) / 2) padding: calc((var(--env-var-height-2) - var(--env-var-font-size-medium) * 1.2) / 2)
var(--env-var-spacing-1-minus) !important; var(--env-var-spacing-1-minus) !important;
} }
} }
@media (max-width: 900px) { @media (max-width: 900px) {
.wallet-adapter-modal-wrapper { .wallet-adapter-modal-wrapper {
flex-direction: column !important; flex-direction: column !important;
} }
.wallet-adapter-modal-divider { .wallet-adapter-modal-divider {
margin: var(--spacing-sm) 0 !important; margin: var(--spacing-sm) 0 !important;
} }
} }
+5
View File
@@ -20,6 +20,7 @@ const initialState = {
collapsed: false, collapsed: false,
}, },
mode: initialMode, mode: initialMode,
showURL: false,
greeting: { index: 0, lastUpdate: null }, greeting: { index: 0, lastUpdate: null },
timezone: "America/Toronto", timezone: "America/Toronto",
distributedUptimeEnabled: false, distributedUptimeEnabled: false,
@@ -46,6 +47,9 @@ const uiSlice = createSlice({
setMode: (state, action) => { setMode: (state, action) => {
state.mode = action.payload; state.mode = action.payload;
}, },
setShowURL: (state, action) => {
state.showURL = action.payload;
},
setGreeting(state, action) { setGreeting(state, action) {
state.greeting.index = action.payload.index; state.greeting.index = action.payload.index;
state.greeting.lastUpdate = action.payload.lastUpdate; state.greeting.lastUpdate = action.payload.lastUpdate;
@@ -67,6 +71,7 @@ export const {
setRowsPerPage, setRowsPerPage,
toggleSidebar, toggleSidebar,
setMode, setMode,
setShowURL,
setGreeting, setGreeting,
setTimezone, setTimezone,
setDistributedUptimeEnabled, setDistributedUptimeEnabled,
+18 -18
View File
@@ -2,26 +2,26 @@ import { useState } from "react";
import { networkService } from "../main"; import { networkService } from "../main";
export const useBulkMonitors = () => { export const useBulkMonitors = () => {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const createBulkMonitors = async (file, user) => { const createBulkMonitors = async (file, user) => {
setIsLoading(true); setIsLoading(true);
const formData = new FormData(); const formData = new FormData();
formData.append("csvFile", file); formData.append("csvFile", file);
formData.append("userId", user._id); formData.append("userId", user._id);
formData.append("teamId", user.teamId); formData.append("teamId", user.teamId);
try { try {
const response = await networkService.createBulkMonitors(formData); const response = await networkService.createBulkMonitors(formData);
return [true, response.data, null]; // [success, data, error] return [true, response.data, null]; // [success, data, error]
} catch (err) { } catch (err) {
const errorMessage = err?.response?.data?.msg || err.message; const errorMessage = err?.response?.data?.msg || err.message;
return [false, null, errorMessage]; return [false, null, errorMessage];
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
return [createBulkMonitors, isLoading]; return [createBulkMonitors, isLoading];
}; };
@@ -3,13 +3,12 @@ import { networkService } from "../main";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { createToast } from "../Utils/toastUtils"; import { createToast } from "../Utils/toastUtils";
export const useFetchUptimeMonitorDetails = ({ monitorId, dateRange }) => { export const useFetchUptimeMonitorDetails = ({ monitorId, dateRange, trigger }) => {
const [networkError, setNetworkError] = useState(false); const [networkError, setNetworkError] = useState(false);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [monitor, setMonitor] = useState(undefined); const [monitor, setMonitor] = useState(undefined);
const [monitorStats, setMonitorStats] = useState(undefined); const [monitorStats, setMonitorStats] = useState(undefined);
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
const fetchMonitors = async () => { const fetchMonitors = async () => {
try { try {
@@ -29,7 +28,7 @@ export const useFetchUptimeMonitorDetails = ({ monitorId, dateRange }) => {
} }
}; };
fetchMonitors(); fetchMonitors();
}, [dateRange, monitorId, navigate]); }, [dateRange, monitorId, navigate, trigger]);
return [monitor, monitorStats, isLoading, networkError]; return [monitor, monitorStats, isLoading, networkError];
}; };
+28
View File
@@ -0,0 +1,28 @@
import { useState } from "react";
import { networkService } from "../main";
import { createToast } from "../Utils/toastUtils";
const usePauseMonitor = ({ monitorId, triggerUpdate }) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
const pauseMonitor = async () => {
try {
setIsLoading(true);
const res = await networkService.pauseMonitorById({ monitorId });
createToast({
body: res.data.data.isActive
? "Monitor resumed successfully"
: "Monitor paused successfully",
});
triggerUpdate();
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
};
return [pauseMonitor, isLoading, error];
};
export { usePauseMonitor };
+11 -1
View File
@@ -1,4 +1,5 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useTheme } from "@mui/material";
const useMonitorUtils = () => { const useMonitorUtils = () => {
const getMonitorWithPercentage = useCallback((monitor, theme) => { const getMonitorWithPercentage = useCallback((monitor, theme) => {
@@ -36,7 +37,16 @@ const useMonitorUtils = () => {
return monitor?.status == true ? "up" : "down"; return monitor?.status == true ? "up" : "down";
}, []); }, []);
return { getMonitorWithPercentage, determineState }; const theme = useTheme();
const statusColor = {
up: theme.palette.success.lowContrast,
down: theme.palette.error.lowContrast,
paused: theme.palette.warning.lowContrast,
pending: theme.palette.warning.lowContrast,
};
return { getMonitorWithPercentage, determineState, statusColor };
}; };
export { useMonitorUtils }; export { useMonitorUtils };
+31 -4
View File
@@ -2,27 +2,54 @@ import { useState } from "react";
import { networkService } from "../main"; import { networkService } from "../main";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { createToast } from "../Utils/toastUtils"; import { createToast } from "../Utils/toastUtils";
import { useTranslation } from "react-i18next";
const useSendTestEmail = () => { const useSendTestEmail = () => {
const [isSending, setIsSending] = useState(false); const [isSending, setIsSending] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const user = useSelector((state) => state.auth.user); const user = useSelector((state) => state.auth.user);
const { t } = useTranslation();
const sendTestEmail = async () => { /**
* Send a test email with optional email configuration
* @param {Object} emailConfig - Optional email configuration parameters
*/
const sendTestEmail = async (emailConfig = null) => {
try { try {
setIsSending(true); setIsSending(true);
setError(null); setError(null);
const response = await networkService.sendTestEmail({ to: user.email }); // Send the test email with or without configuration
const response = await networkService.sendTestEmail({
to: user.email,
emailConfig,
});
if (typeof response?.data?.data?.messageId !== "undefined") { if (typeof response?.data?.data?.messageId !== "undefined") {
createToast({ createToast({
body: "Test email sent successfully", body: t("settingsTestEmailSuccess", "Test email sent successfully"),
}); });
} else { } else {
throw new Error("Failed to send test email"); throw new Error(
response?.data?.error ||
t("settingsTestEmailFailed", "Failed to send test email")
);
} }
} catch (error) { } catch (error) {
createToast({
body: t("failedToSendEmail"),
});
setError(error); setError(error);
createToast({
body: t(
"settingsTestEmailFailedWithReason",
"Failed to send test email: {{reason}}",
{
reason: error.message || t("settingsTestEmailUnknownError", "Unknown error"),
}
),
variant: "error",
});
} finally { } finally {
setIsSending(false); setIsSending(false);
} }
+3 -3
View File
@@ -1,6 +1,6 @@
import React from 'react'; import React from "react";
import AppBar from '../../Components/Common/AppBar'; import AppBar from "../../Components/Common/AppBar";
import Footer from '../../Components/Common/Footer'; import Footer from "../../Components/Common/Footer";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const AboutUs = () => { const AboutUs = () => {
+8 -6
View File
@@ -31,21 +31,19 @@ const Account = ({ open = "profile" }) => {
let tabList = [ let tabList = [
{ name: t("menu.profile"), value: "profile" }, { name: t("menu.profile"), value: "profile" },
{ name: t("menu.password"), value: "password" }, { name: t("menu.password"), value: "password" },
{ name: t("menu.team"), value: "team" } { name: t("menu.team"), value: "team" },
]; ];
const hideTeams = !requiredRoles.some((role) => user.role.includes(role)); const hideTeams = !requiredRoles.some((role) => user.role.includes(role));
if (hideTeams) { if (hideTeams) {
tabList = [ tabList = [
{ name: t("menu.profile"), value: "profile" }, { name: t("menu.profile"), value: "profile" },
{ name: t("menu.password"), value: "password" } { name: t("menu.password"), value: "password" },
]; ];
} }
// Remove password for demo // Remove password for demo
if (user.role.includes("demo")) { if (user.role.includes("demo")) {
tabList = [ tabList = [{ name: t("menu.profile"), value: "profile" }];
{ name: t("menu.profile"), value: "profile" }
];
} }
const handleKeyDown = (event) => { const handleKeyDown = (event) => {
@@ -70,7 +68,11 @@ const Account = ({ open = "profile" }) => {
py={theme.spacing(12)} py={theme.spacing(12)}
> >
<TabContext value={tab}> <TabContext value={tab}>
<CustomTabList value={tab} onChange={handleTabChange} aria-label="account tabs"> <CustomTabList
value={tab}
onChange={handleTabChange}
aria-label="account tabs"
>
{tabList.map((item, index) => ( {tabList.map((item, index) => (
<Tab <Tab
label={item.name} label={item.name}
@@ -75,11 +75,11 @@ const EmailStep = ({ form, errors, onSubmit, onChange }) => {
width: "30%", width: "30%",
px: theme.spacing(6), px: theme.spacing(6),
borderRadius: `${theme.shape.borderRadius}px !important`, borderRadius: `${theme.shape.borderRadius}px !important`,
'&.MuiButtonBase-root': { "&.MuiButtonBase-root": {
borderRadius: `${theme.shape.borderRadius}px !important` borderRadius: `${theme.shape.borderRadius}px !important`,
}, },
'&.MuiButton-root': { "&.MuiButton-root": {
borderRadius: `${theme.shape.borderRadius}px !important` borderRadius: `${theme.shape.borderRadius}px !important`,
}, },
"&.Mui-focusVisible": { "&.Mui-focusVisible": {
outline: `2px solid ${theme.palette.primary.main}`, outline: `2px solid ${theme.palette.primary.main}`,
@@ -79,11 +79,11 @@ const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
sx={{ sx={{
px: theme.spacing(5), px: theme.spacing(5),
borderRadius: `${theme.shape.borderRadius}px !important`, borderRadius: `${theme.shape.borderRadius}px !important`,
'&.MuiButtonBase-root': { "&.MuiButtonBase-root": {
borderRadius: `${theme.shape.borderRadius}px !important` borderRadius: `${theme.shape.borderRadius}px !important`,
}, },
'&.MuiButton-root': { "&.MuiButton-root": {
borderRadius: `${theme.shape.borderRadius}px !important` borderRadius: `${theme.shape.borderRadius}px !important`,
}, },
"& svg.MuiSvgIcon-root": { "& svg.MuiSvgIcon-root": {
mr: theme.spacing(3), mr: theme.spacing(3),
@@ -108,11 +108,11 @@ const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
width: "30%", width: "30%",
px: theme.spacing(4), px: theme.spacing(4),
borderRadius: `${theme.shape.borderRadius}px !important`, borderRadius: `${theme.shape.borderRadius}px !important`,
'&.MuiButtonBase-root': { "&.MuiButtonBase-root": {
borderRadius: `${theme.shape.borderRadius}px !important` borderRadius: `${theme.shape.borderRadius}px !important`,
}, },
'&.MuiButton-root': { "&.MuiButton-root": {
borderRadius: `${theme.shape.borderRadius}px !important` borderRadius: `${theme.shape.borderRadius}px !important`,
}, },
"&.Mui-focusVisible": { "&.Mui-focusVisible": {
outline: `2px solid ${theme.palette.primary.main}`, outline: `2px solid ${theme.palette.primary.main}`,
+13 -11
View File
@@ -43,8 +43,6 @@ const Login = () => {
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
const [step, setStep] = useState(0); const [step, setStep] = useState(0);
useEffect(() => { useEffect(() => {
if (authToken) { if (authToken) {
navigate("/uptime"); navigate("/uptime");
@@ -84,7 +82,9 @@ const Login = () => {
); );
if (error) { if (error) {
const errorMessage = error.details[0].message; const errorMessage = error.details[0].message;
const translatedMessage = errorMessage.startsWith('auth') ? t(errorMessage) : errorMessage; const translatedMessage = errorMessage.startsWith("auth")
? t(errorMessage)
: errorMessage;
setErrors((prev) => ({ ...prev, email: translatedMessage })); setErrors((prev) => ({ ...prev, email: translatedMessage }));
createToast({ body: translatedMessage }); createToast({ body: translatedMessage });
} else { } else {
@@ -103,7 +103,9 @@ const Login = () => {
createToast({ createToast({
body: body:
error.details && error.details.length > 0 error.details && error.details.length > 0
? (error.details[0].message.startsWith('auth') ? t(error.details[0].message) : error.details[0].message) ? error.details[0].message.startsWith("auth")
? t(error.details[0].message)
: error.details[0].message
: t("Error validating data."), : t("Error validating data."),
}); });
} else { } else {
@@ -227,9 +229,9 @@ const Login = () => {
email={form.email} email={form.email}
errorEmail={errors.email} errorEmail={errors.email}
/> />
{/* Registration link */} {/* Registration link */}
<Box textAlign="center" > <Box textAlign="center">
<Typography <Typography
className="forgot-p" className="forgot-p"
display="inline-block" display="inline-block"
@@ -241,11 +243,11 @@ const Login = () => {
component="span" component="span"
color={theme.palette.accent.main} color={theme.palette.accent.main}
ml={theme.spacing(2)} ml={theme.spacing(2)}
sx={{ sx={{
cursor: 'pointer', cursor: "pointer",
'&:hover': { "&:hover": {
color: theme.palette.accent.darker color: theme.palette.accent.darker,
} },
}} }}
onClick={() => navigate("/register")} onClick={() => navigate("/register")}
> >
@@ -100,9 +100,7 @@ const NewPasswordConfirmed = () => {
</IconBox> </IconBox>
</Stack> </Stack>
<Typography component="h1">{t("passwordreset")}</Typography> <Typography component="h1">{t("passwordreset")}</Typography>
<Typography mb={theme.spacing(2)}> <Typography mb={theme.spacing(2)}>{t("authNewPasswordConfirmed")}</Typography>
{t("authNewPasswordConfirmed")}
</Typography>
</Box> </Box>
<Button <Button
variant="contained" variant="contained"
+1 -1
View File
@@ -105,7 +105,7 @@ const LandingPage = ({ isSuperAdmin, onSignup }) => {
); );
}} }}
/> />
) ),
}} }}
/> />
</Typography> </Typography>
@@ -68,11 +68,14 @@ function StepTwo({ form, errors, onSubmit, onChange, onBack }) {
onInput={(e) => (e.target.value = e.target.value.toLowerCase())} onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
onChange={onChange} onChange={onChange}
error={errors.email ? true : false} error={errors.email ? true : false}
helperText={errors.email && ( helperText={
errors.email.includes("required") ? t("authRegisterEmailRequired") : errors.email &&
errors.email.includes("valid email") ? t("authRegisterEmailInvalid") : (errors.email.includes("required")
t(errors.email) ? t("authRegisterEmailRequired")
)} : errors.email.includes("valid email")
? t("authRegisterEmailInvalid")
: t(errors.email))
}
ref={inputRef} ref={inputRef}
/> />
<Stack <Stack
@@ -199,9 +199,7 @@ const CreateDistributedUptime = () => {
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h2">{t("settingsGeneralSettings")}</Typography> <Typography component="h2">{t("settingsGeneralSettings")}</Typography>
<Typography component="p"> <Typography component="p">{t("distributedUptimeCreateSelectURL")}</Typography>
{t("distributedUptimeCreateSelectURL")}
</Typography>
</Box> </Box>
<Stack gap={theme.spacing(15)}> <Stack gap={theme.spacing(15)}>
<TextInput <TextInput
@@ -293,7 +291,9 @@ const CreateDistributedUptime = () => {
</ConfigBox> </ConfigBox>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h2">{t("distributedUptimeCreateIncidentNotification")}</Typography> <Typography component="h2">
{t("distributedUptimeCreateIncidentNotification")}
</Typography>
<Typography component="p"> <Typography component="p">
{t("distributedUptimeCreateIncidentDescription")} {t("distributedUptimeCreateIncidentDescription")}
</Typography> </Typography>
@@ -312,7 +312,9 @@ const CreateDistributedUptime = () => {
</ConfigBox> </ConfigBox>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h2">{t("distributedUptimeCreateAdvancedSettings")}</Typography> <Typography component="h2">
{t("distributedUptimeCreateAdvancedSettings")}
</Typography>
</Box> </Box>
<Stack gap={theme.spacing(12)}> <Stack gap={theme.spacing(12)}>
<Select <Select
@@ -17,7 +17,7 @@ const MESSAGES = [
const ChatBot = ({ sx }) => { const ChatBot = ({ sx }) => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<ColContainer <ColContainer
backgroundColor={theme.palette.chatbot.background} backgroundColor={theme.palette.chatbot.background}
@@ -12,7 +12,7 @@ import { useTranslation } from "react-i18next";
const Controls = ({ isDeleteOpen, setIsDeleteOpen, isDeleting, monitorId }) => { const Controls = ({ isDeleteOpen, setIsDeleteOpen, isDeleting, monitorId }) => {
const theme = useTheme(); const theme = useTheme();
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Stack <Stack
@@ -76,7 +76,9 @@ const DeviceTicker = ({ data, width = "100%", connectionStatus }) => {
<Typography>{city}</Typography> <Typography>{city}</Typography>
</td> </td>
<td style={{ textAlign: "right", paddingLeft: theme.spacing(4) }}> <td style={{ textAlign: "right", paddingLeft: theme.spacing(4) }}>
<Typography>{Math.floor(dataPoint.responseTime)} {t("ms")}</Typography> <Typography>
{Math.floor(dataPoint.responseTime)} {t("ms")}
</Typography>
</td> </td>
<td style={{ textAlign: "right", paddingLeft: theme.spacing(4) }}> <td style={{ textAlign: "right", paddingLeft: theme.spacing(4) }}>
<Typography color={theme.palette.warning.main}> <Typography color={theme.palette.warning.main}>
@@ -10,9 +10,7 @@ const MonitorHeader = ({ monitor }) => {
<Stack direction="row"> <Stack direction="row">
<Stack gap={theme.spacing(2)}> <Stack gap={theme.spacing(2)}>
<Typography variant="h1">{monitor.name}</Typography> <Typography variant="h1">{monitor.name}</Typography>
<Typography variant="h2"> <Typography variant="h2">{t("distributedUptimeDetailsMonitorHeader")}</Typography>
{t("distributedUptimeDetailsMonitorHeader")}
</Typography>
</Stack> </Stack>
</Stack> </Stack>
); );
@@ -2,7 +2,14 @@ import { useState, useEffect } from "react";
import { networkService } from "../../../main"; import { networkService } from "../../../main";
import { createToast } from "../../../Utils/toastUtils"; import { createToast } from "../../../Utils/toastUtils";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
const useChecksFetch = ({ selectedMonitor, selectedMonitorType, filter, dateRange, page, rowsPerPage }) => { const useChecksFetch = ({
selectedMonitor,
selectedMonitorType,
filter,
dateRange,
page,
rowsPerPage,
}) => {
//Redux //Redux
const { user } = useSelector((state) => state.auth); const { user } = useSelector((state) => state.auth);
@@ -30,7 +30,7 @@ const useMonitorsFetch = ({ teamId }) => {
acc[monitor._id] = { acc[monitor._id] = {
_id: monitor._id, _id: monitor._id,
name: monitor.name, name: monitor.name,
type: monitor.type type: monitor.type,
}; };
return acc; return acc;
}, {}); }, {});
+4 -2
View File
@@ -18,8 +18,10 @@ const Incidents = () => {
// Redux state // Redux state
const { user } = useSelector((state) => state.auth); const { user } = useSelector((state) => state.auth);
const { t } = useTranslation(); const { t } = useTranslation();
const BREADCRUMBS = [{ name: t("incidentsPageTitle", "Incidents"), path: "/incidents" }]; const BREADCRUMBS = [
{ name: t("incidentsPageTitle", "Incidents"), path: "/incidents" },
];
// Local state // Local state
const [selectedMonitor, setSelectedMonitor] = useState("0"); const [selectedMonitor, setSelectedMonitor] = useState("0");
@@ -6,7 +6,10 @@ import { useSelector, useDispatch } from "react-redux";
// Utility and Network // Utility and Network
import { infrastructureMonitorValidation } from "../../../Validation/validation"; import { infrastructureMonitorValidation } from "../../../Validation/validation";
import { createInfrastructureMonitor, updateInfrastructureMonitor } from "../../../Features/InfrastructureMonitors/infrastructureMonitorsSlice"; import {
createInfrastructureMonitor,
updateInfrastructureMonitor,
} from "../../../Features/InfrastructureMonitors/infrastructureMonitorsSlice";
import { useHardwareMonitorsFetch } from "../Details/Hooks/useHardwareMonitorsFetch"; import { useHardwareMonitorsFetch } from "../Details/Hooks/useHardwareMonitorsFetch";
import { capitalizeFirstLetter } from "../../../Utils/stringUtils"; import { capitalizeFirstLetter } from "../../../Utils/stringUtils";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -55,7 +58,7 @@ const CreateInfrastructureMonitor = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const { monitorId } = useParams(); const { monitorId } = useParams();
const { t } = useTranslation(); const { t } = useTranslation();
// Determine if we are creating or editing // Determine if we are creating or editing
const isCreate = typeof monitorId === "undefined"; const isCreate = typeof monitorId === "undefined";
@@ -90,20 +93,26 @@ const CreateInfrastructureMonitor = () => {
setInfrastructureMonitor({ setInfrastructureMonitor({
url: monitor.url.replace(/^https?:\/\//, ""), url: monitor.url.replace(/^https?:\/\//, ""),
name: monitor.name || "", name: monitor.name || "",
notifications: monitor.notifications?.filter(n => typeof n === "object") || [], notifications: monitor.notifications?.filter((n) => typeof n === "object") || [],
notify_email: (monitor.notifications?.length ?? 0) > 0, notify_email: (monitor.notifications?.length ?? 0) > 0,
interval: monitor.interval / MS_PER_MINUTE, interval: monitor.interval / MS_PER_MINUTE,
cpu: monitor.thresholds?.usage_cpu !== undefined, cpu: monitor.thresholds?.usage_cpu !== undefined,
usage_cpu: monitor.thresholds?.usage_cpu ? monitor.thresholds.usage_cpu * 100 : "", usage_cpu: monitor.thresholds?.usage_cpu ? monitor.thresholds.usage_cpu * 100 : "",
memory: monitor.thresholds?.usage_memory !== undefined, memory: monitor.thresholds?.usage_memory !== undefined,
usage_memory: monitor.thresholds?.usage_memory ? monitor.thresholds.usage_memory * 100 : "", usage_memory: monitor.thresholds?.usage_memory
? monitor.thresholds.usage_memory * 100
: "",
disk: monitor.thresholds?.usage_disk !== undefined, disk: monitor.thresholds?.usage_disk !== undefined,
usage_disk: monitor.thresholds?.usage_disk ? monitor.thresholds.usage_disk * 100 : "", usage_disk: monitor.thresholds?.usage_disk
? monitor.thresholds.usage_disk * 100
: "",
temperature: monitor.thresholds?.usage_temperature !== undefined, temperature: monitor.thresholds?.usage_temperature !== undefined,
usage_temperature: monitor.thresholds?.usage_temperature ? monitor.thresholds.usage_temperature * 100 : "", usage_temperature: monitor.thresholds?.usage_temperature
? monitor.thresholds.usage_temperature * 100
: "",
secret: monitor.secret || "", secret: monitor.secret || "",
}); });
setHttps(monitor.url.startsWith("https")); setHttps(monitor.url.startsWith("https"));
@@ -199,7 +208,9 @@ const CreateInfrastructureMonitor = () => {
: await dispatch(updateInfrastructureMonitor({ monitorId, monitor: form })); : await dispatch(updateInfrastructureMonitor({ monitorId, monitor: form }));
if (action.meta.requestStatus === "fulfilled") { if (action.meta.requestStatus === "fulfilled") {
createToast({ createToast({
body: isCreate ? t("infrastructureMonitorCreated") : t("infrastructureMonitorUpdated"), body: isCreate
? t("infrastructureMonitorCreated")
: t("infrastructureMonitorUpdated"),
}); });
navigate("/infrastructure"); navigate("/infrastructure");
} else { } else {
@@ -236,22 +247,23 @@ const CreateInfrastructureMonitor = () => {
const handleNotifications = (event, type) => { const handleNotifications = (event, type) => {
const { value, checked } = event.target; const { value, checked } = event.target;
let notifications = [...infrastructureMonitor.notifications]; let notifications = [...infrastructureMonitor.notifications];
if (checked) { if (checked) {
if (!notifications.some((n) => n.type === type && n.address === value)) { if (!notifications.some((n) => n.type === type && n.address === value)) {
notifications.push({ type, address: value }); notifications.push({ type, address: value });
} }
} else { } else {
notifications = notifications.filter((n) => !(n.type === type && n.address === value)); notifications = notifications.filter(
(n) => !(n.type === type && n.address === value)
);
} }
setInfrastructureMonitor((prev) => ({ setInfrastructureMonitor((prev) => ({
...prev, ...prev,
notifications, notifications,
...(type === "email" ? { notify_email: checked } : {}) ...(type === "email" ? { notify_email: checked } : {}),
})); }));
}; };
return ( return (
<Box className="create-infrastructure-monitor"> <Box className="create-infrastructure-monitor">
@@ -260,8 +272,10 @@ const CreateInfrastructureMonitor = () => {
{ name: "Infrastructure monitors", path: "/infrastructure" }, { name: "Infrastructure monitors", path: "/infrastructure" },
...(isCreate ...(isCreate
? [{ name: "Create", path: "/infrastructure/create" }] ? [{ name: "Create", path: "/infrastructure/create" }]
: [{ name: "Details", path: `/infrastructure/${monitorId}` }, { name: "Configure", path: `/infrastructure/configure/${monitorId}` }] : [
), { name: "Details", path: `/infrastructure/${monitorId}` },
{ name: "Configure", path: `/infrastructure/configure/${monitorId}` },
]),
]} ]}
/> />
<Stack <Stack
@@ -281,7 +295,7 @@ const CreateInfrastructureMonitor = () => {
component="span" component="span"
fontSize="inherit" fontSize="inherit"
> >
{t(isCreate? "infrastructureCreateYour": "infrastructureEditYour")}{" "} {t(isCreate ? "infrastructureCreateYour" : "infrastructureEditYour")}{" "}
</Typography> </Typography>
<Typography <Typography
component="span" component="span"
@@ -294,7 +308,12 @@ const CreateInfrastructureMonitor = () => {
</Typography> </Typography>
<ConfigBox> <ConfigBox>
<Stack gap={theme.spacing(6)}> <Stack gap={theme.spacing(6)}>
<Typography component="h2">{t("settingsGeneralSettings")}</Typography> <Typography
component="h2"
variant="h2"
>
{t("settingsGeneralSettings")}
</Typography>
<Typography component="p"> <Typography component="p">
{t("infrastructureCreateGeneralSettingsDescription")} {t("infrastructureCreateGeneralSettingsDescription")}
</Typography> </Typography>
@@ -323,25 +342,25 @@ const CreateInfrastructureMonitor = () => {
disabled={!isCreate} disabled={!isCreate}
/> />
{isCreate && ( {isCreate && (
<Box> <Box>
<Typography component="p">{t("infrastructureProtocol")}</Typography> <Typography component="p">{t("infrastructureProtocol")}</Typography>
<ButtonGroup> <ButtonGroup>
<Button <Button
variant="group" variant="group"
filled={https.toString()} filled={https.toString()}
onClick={() => setHttps(true)} onClick={() => setHttps(true)}
> >
{t("https")} {t("https")}
</Button> </Button>
<Button <Button
variant="group" variant="group"
filled={(!https).toString()} filled={(!https).toString()}
onClick={() => setHttps(false)} onClick={() => setHttps(false)}
> >
{t("http")} {t("http")}
</Button> </Button>
</ButtonGroup> </ButtonGroup>
</Box> </Box>
)} )}
<TextInput <TextInput
type="text" type="text"
@@ -368,7 +387,12 @@ const CreateInfrastructureMonitor = () => {
</ConfigBox> </ConfigBox>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h2">{t("distributedUptimeCreateIncidentNotification")}</Typography> <Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateIncidentNotification")}
</Typography>
<Typography component="p"> <Typography component="p">
{t("distributedUptimeCreateIncidentDescription")} {t("distributedUptimeCreateIncidentDescription")}
</Typography> </Typography>
@@ -385,7 +409,12 @@ const CreateInfrastructureMonitor = () => {
</ConfigBox> </ConfigBox>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h2">{t("infrastructureCustomizeAlerts")}</Typography> <Typography
component="h2"
variant="h2"
>
{t("infrastructureCustomizeAlerts")}
</Typography>
<Typography component="p"> <Typography component="p">
{t("infrastructureAlertNotificationDescription")} {t("infrastructureAlertNotificationDescription")}
</Typography> </Typography>
@@ -432,7 +461,12 @@ const CreateInfrastructureMonitor = () => {
</ConfigBox> </ConfigBox>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h2">{t("distributedUptimeCreateAdvancedSettings")}</Typography> <Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateAdvancedSettings")}
</Typography>
</Box> </Box>
<Stack gap={theme.spacing(12)}> <Stack gap={theme.spacing(12)}>
<Select <Select
@@ -455,7 +489,7 @@ const CreateInfrastructureMonitor = () => {
onClick={handleCreateInfrastructureMonitor} onClick={handleCreateInfrastructureMonitor}
loading={monitorState?.isLoading} loading={monitorState?.isLoading}
> >
{t(isCreate? "infrastructureCreateMonitor": "infrastructureEditMonitor")} {t(isCreate ? "infrastructureCreateMonitor" : "infrastructureEditMonitor")}
</Button> </Button>
</Stack> </Stack>
</Stack> </Stack>
@@ -16,8 +16,8 @@ const Gauge = ({ value, heading, metricOne, valueOne, metricTwo, valueTwo }) =>
mb: theme.spacing(2), mb: theme.spacing(2),
mt: theme.spacing(2), mt: theme.spacing(2),
pr: theme.spacing(2), pr: theme.spacing(2),
textAlign: 'right' textAlign: "right",
} };
return ( return (
<BaseContainer> <BaseContainer>
@@ -32,14 +32,19 @@ const Gauge = ({ value, heading, metricOne, valueOne, metricTwo, valueTwo }) =>
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
width: "100%", width: "100%",
backgroundColor: theme.palette.gradient.color1 backgroundColor: theme.palette.gradient.color1,
}} }}
> >
<CustomGauge <CustomGauge
progress={value} progress={value}
radius={100} radius={100}
/> />
<Typography component="h2" sx={{ fontWeight: 600 }}>{heading}</Typography> <Typography
component="h2"
sx={{ fontWeight: 600 }}
>
{heading}
</Typography>
</Box> </Box>
<Box <Box
sx={{ sx={{
@@ -2,11 +2,10 @@ import { useEffect, useState } from "react";
import { networkService } from "../../../../main"; import { networkService } from "../../../../main";
const useHardwareMonitorsFetch = ({ monitorId, dateRange }) => { const useHardwareMonitorsFetch = ({ monitorId, dateRange }) => {
// Abort early if creating monitor // Abort early if creating monitor
if (!monitorId) { if (!monitorId) {
return { monitor: undefined, isLoading: false, networkError: undefined }; return { monitor: undefined, isLoading: false, networkError: undefined };
} }
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [networkError, setNetworkError] = useState(false); const [networkError, setNetworkError] = useState(false);
const [monitor, setMonitor] = useState(undefined); const [monitor, setMonitor] = useState(undefined);
@@ -53,10 +53,10 @@ const Filter = ({
return ( return (
<Box <Box
sx={{ sx={{
m: theme.spacing(2), m: theme.spacing(2),
ml: theme.spacing(4), ml: theme.spacing(4),
}} }}
> >
<FilterHeader <FilterHeader
header={t("status")} header={t("status")}
@@ -80,10 +80,10 @@ const Filter = ({
}; };
Filter.propTypes = { Filter.propTypes = {
selectedStatus: PropTypes.arrayOf(PropTypes.string), selectedStatus: PropTypes.arrayOf(PropTypes.string),
setSelectedStatus: PropTypes.func.isRequired, setSelectedStatus: PropTypes.func.isRequired,
setToFilterStatus: PropTypes.func.isRequired, setToFilterStatus: PropTypes.func.isRequired,
handleReset: PropTypes.func.isRequired, handleReset: PropTypes.func.isRequired,
}; };
export default Filter; export default Filter;
@@ -98,9 +98,6 @@ const MonitorsTable = ({ shouldRender, monitors, isAdmin, handleActionMenuDelete
const mem = (monitor?.checks[0]?.memory.usage_percent ?? 0) * 100; const mem = (monitor?.checks[0]?.memory.usage_percent ?? 0) * 100;
const disk = (monitor?.checks[0]?.disk[0]?.usage_percent ?? 0) * 100; const disk = (monitor?.checks[0]?.disk[0]?.usage_percent ?? 0) * 100;
const status = determineState(monitor); const status = determineState(monitor);
const uptimePercentage = ((monitor?.uptimePercentage ?? 0) * 100)
.toFixed(2)
.toString();
const percentageColor = const percentageColor =
monitor.uptimePercentage < 0.25 monitor.uptimePercentage < 0.25
? theme.palette.error.main ? theme.palette.error.main
@@ -117,7 +114,6 @@ const MonitorsTable = ({ shouldRender, monitors, isAdmin, handleActionMenuDelete
mem, mem,
disk, disk,
status, status,
uptimePercentage,
percentageColor, percentageColor,
}; };
}); });
@@ -73,7 +73,7 @@ const InfrastructureMonitors = () => {
); );
} }
if (!isLoading && typeof summary?.totalMonitors === "undefined" ) { if (!isLoading && typeof summary?.totalMonitors === "undefined") {
return ( return (
<Fallback <Fallback
vowelStart={true} vowelStart={true}
+1 -3
View File
@@ -134,9 +134,7 @@ const Integrations = () => {
}} }}
> >
<Typography component="h1">{t("integrations")}</Typography> <Typography component="h1">{t("integrations")}</Typography>
<Typography mb={theme.spacing(12)}> <Typography mb={theme.spacing(12)}>{t("integrationsPrism")}</Typography>
{t("integrationsPrism")}
</Typography>
<Grid <Grid
container container
spacing={theme.spacing(12)} spacing={theme.spacing(12)}
@@ -137,7 +137,7 @@ const CreateMaintenance = () => {
const response = await networkService.getMonitorsByTeamId({ const response = await networkService.getMonitorsByTeamId({
teamId: user.teamId, teamId: user.teamId,
limit: null, limit: null,
types: ["http", "ping", "pagespeed"], types: ["http", "ping", "pagespeed", "port"],
}); });
const monitors = response.data.data.monitors; const monitors = response.data.data.monitors;
setMonitors(monitors); setMonitors(monitors);
@@ -50,7 +50,7 @@ const MaintenanceTable = ({
); );
setPage(0); setPage(0);
}; };
const { t } = useTranslation(); const { t } = useTranslation();
const headers = [ const headers = [
+19 -6
View File
@@ -310,7 +310,12 @@ const PageSpeedConfigure = () => {
</Stack> </Stack>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h2">{t("settingsGeneralSettings")}</Typography> <Typography
component="h2"
variant="h2"
>
{t("settingsGeneralSettings")}
</Typography>
<Typography component="p"> <Typography component="p">
{t("pageSpeedConfigureSettingsDescription")} {t("pageSpeedConfigureSettingsDescription")}
</Typography> </Typography>
@@ -349,7 +354,12 @@ const PageSpeedConfigure = () => {
</ConfigBox> </ConfigBox>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h2">{t("distributedUptimeCreateIncidentNotification")}</Typography> <Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateIncidentNotification")}
</Typography>
<Typography component="p"> <Typography component="p">
{t("distributedUptimeCreateIncidentDescription")} {t("distributedUptimeCreateIncidentDescription")}
</Typography> </Typography>
@@ -394,9 +404,7 @@ const PageSpeedConfigure = () => {
value="" value=""
onChange={() => logger.warn("disabled")} onChange={() => logger.warn("disabled")}
/> />
<Typography mt={theme.spacing(4)}> <Typography mt={theme.spacing(4)}>{t("seperateEmails")}</Typography>
{t("seperateEmails")}
</Typography>
</Box> </Box>
) : ( ) : (
"" ""
@@ -405,7 +413,12 @@ const PageSpeedConfigure = () => {
</ConfigBox> </ConfigBox>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h2">{t("distributedUptimeCreateAdvancedSettings")}</Typography> <Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateAdvancedSettings")}
</Typography>
</Box> </Box>
<Stack gap={theme.spacing(20)}> <Stack gap={theme.spacing(20)}>
<Select <Select
+24 -6
View File
@@ -216,10 +216,13 @@ const CreatePageSpeed = () => {
</Typography> </Typography>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h2">{t("settingsGeneralSettings")}</Typography> <Typography
<Typography component="p"> component="h2"
{t("distributedUptimeCreateSelectURL")} variant="h2"
>
{t("settingsGeneralSettings")}
</Typography> </Typography>
<Typography component="p">{t("distributedUptimeCreateSelectURL")}</Typography>
</Box> </Box>
<Stack gap={theme.spacing(15)}> <Stack gap={theme.spacing(15)}>
<TextInput <TextInput
@@ -251,7 +254,12 @@ const CreatePageSpeed = () => {
</ConfigBox> </ConfigBox>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h2">{t("distributedUptimeCreateChecks")}</Typography> <Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateChecks")}
</Typography>
<Typography component="p"> <Typography component="p">
{t("distributedUptimeCreateChecksDescription")} {t("distributedUptimeCreateChecksDescription")}
</Typography> </Typography>
@@ -300,7 +308,12 @@ const CreatePageSpeed = () => {
</ConfigBox> </ConfigBox>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h2">{t("distributedUptimeCreateIncidentNotification")}</Typography> <Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateIncidentNotification")}
</Typography>
<Typography component="p"> <Typography component="p">
{t("distributedUptimeCreateIncidentDescription")} {t("distributedUptimeCreateIncidentDescription")}
</Typography> </Typography>
@@ -320,7 +333,12 @@ const CreatePageSpeed = () => {
</ConfigBox> </ConfigBox>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h2">{t("distributedUptimeCreateAdvancedSettings")}</Typography> <Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateAdvancedSettings")}
</Typography>
</Box> </Box>
<Stack gap={theme.spacing(12)}> <Stack gap={theme.spacing(12)}>
<Select <Select
@@ -10,7 +10,7 @@ import { useTranslation } from "react-i18next";
const PerformanceReport = ({ shouldRender, audits }) => { const PerformanceReport = ({ shouldRender, audits }) => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
if (!shouldRender) { if (!shouldRender) {
return <SkeletonLayout />; return <SkeletonLayout />;
} }
+150 -142
View File
@@ -12,150 +12,158 @@ import ThemeSwitch from "../Components/ThemeSwitch";
import LanguageSelector from "../Components/LanguageSelector"; import LanguageSelector from "../Components/LanguageSelector";
const ServerUnreachable = () => { const ServerUnreachable = () => {
const theme = useTheme(); const theme = useTheme();
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation(); const { t } = useTranslation();
// State for tracking connection check status
const [isCheckingConnection, setIsCheckingConnection] = useState(false);
const handleRetry = React.useCallback(async () => { // State for tracking connection check status
setIsCheckingConnection(true); const [isCheckingConnection, setIsCheckingConnection] = useState(false);
try {
// Try to connect to the backend with a simple API call
// We'll use any lightweight endpoint that doesn't require authentication
await networkService.axiosInstance.get('/health', { timeout: 5000 });
// If successful, show toast and navigate to login page
createToast({
body: t("backendReconnected", "Connection to server restored"),
});
navigate("/login");
} catch (error) {
// If still unreachable, stay on this page and show toast
createToast({
body: t("backendStillUnreachable", "Server is still unreachable"),
});
} finally {
setIsCheckingConnection(false);
}
}, [navigate, t]);
return ( const handleRetry = React.useCallback(async () => {
<Stack setIsCheckingConnection(true);
className="login-page auth" try {
overflow="hidden" // Try to connect to the backend with a simple API call
> // We'll use any lightweight endpoint that doesn't require authentication
<Box await networkService.axiosInstance.get("/health", { timeout: 5000 });
className="background-pattern-svg"
sx={{ // If successful, show toast and navigate to login page
"& svg g g:last-of-type path": { createToast({
stroke: theme.palette.primary.lowContrast, body: t("backendReconnected", "Connection to server restored"),
}, });
}} navigate("/login");
> } catch (error) {
<Background style={{ width: "100%" }} /> // If still unreachable, stay on this page and show toast
</Box> createToast({
body: t("backendStillUnreachable", "Server is still unreachable"),
{/* Header with logo */} });
<Stack } finally {
direction="row" setIsCheckingConnection(false);
alignItems="center" }
justifyContent="space-between" }, [navigate, t]);
px={theme.spacing(12)}
gap={theme.spacing(4)} return (
> <Stack
<Stack className="login-page auth"
direction="row" overflow="hidden"
alignItems="center" >
gap={theme.spacing(4)} <Box
> className="background-pattern-svg"
<Logo style={{ borderRadius: theme.shape.borderRadius }} /> sx={{
<Typography sx={{ userSelect: "none" }}>Checkmate</Typography> "& svg g g:last-of-type path": {
</Stack> stroke: theme.palette.primary.lowContrast,
<Stack },
direction="row" }}
spacing={2} >
alignItems="center" <Background style={{ width: "100%" }} />
> </Box>
<LanguageSelector />
<ThemeSwitch /> {/* Header with logo */}
</Stack> <Stack
</Stack> direction="row"
<Stack alignItems="center"
width="100%" justifyContent="space-between"
maxWidth={600} px={theme.spacing(12)}
flex={1} gap={theme.spacing(4)}
justifyContent="center" >
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }} <Stack
pb={theme.spacing(20)} direction="row"
mx="auto" alignItems="center"
rowGap={theme.spacing(8)} gap={theme.spacing(4)}
sx={{ >
"& > .MuiStack-root": { <Logo style={{ borderRadius: theme.shape.borderRadius }} />
border: 1, <Typography sx={{ userSelect: "none" }}>Checkmate</Typography>
borderRadius: theme.spacing(5), </Stack>
borderColor: theme.palette.primary.lowContrast, <Stack
backgroundColor: theme.palette.primary.main, direction="row"
padding: { spacing={2}
xs: theme.spacing(12), alignItems="center"
sm: theme.spacing(20), >
}, <LanguageSelector />
}, <ThemeSwitch />
}} </Stack>
> </Stack>
<Stack spacing={theme.spacing(6)} alignItems="center"> <Stack
<Box sx={{ width="100%"
width: theme.spacing(220), maxWidth={600}
mx: 'auto', flex={1}
'& .alert.row-stack': { justifyContent="center"
width: '100%', px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
alignItems: 'center', pb={theme.spacing(20)}
gap: theme.spacing(3) mx="auto"
} rowGap={theme.spacing(8)}
}}> sx={{
<Alert "& > .MuiStack-root": {
variant="error" border: 1,
body={t("backendUnreachable", "Server Unreachable")} borderRadius: theme.spacing(5),
hasIcon={true} borderColor: theme.palette.primary.lowContrast,
/> backgroundColor: theme.palette.primary.main,
</Box> padding: {
<Box mt={theme.spacing(2)}> xs: theme.spacing(12),
<Typography sm: theme.spacing(20),
variant="body1" },
align="center" },
color={theme.palette.primary.contrastTextSecondary} }}
> >
{t("backendUnreachableMessage", "The Checkmate server is not responding. Please check your deployment configuration or try again later.")} <Stack
</Typography> spacing={theme.spacing(6)}
</Box> alignItems="center"
<Box sx={{ mt: theme.spacing(4) }}> >
<Button <Box
variant="contained" sx={{
color="accent" width: theme.spacing(220),
onClick={handleRetry} mx: "auto",
disabled={isCheckingConnection} "& .alert.row-stack": {
className="dashboard-style-button" width: "100%",
sx={{ alignItems: "center",
px: theme.spacing(6), gap: theme.spacing(3),
borderRadius: `${theme.shape.borderRadius}px !important`, },
'&.MuiButtonBase-root': { }}
borderRadius: `${theme.shape.borderRadius}px !important` >
}, <Alert
'&.MuiButton-root': { variant="error"
borderRadius: `${theme.shape.borderRadius}px !important` body={t("backendUnreachable", "Server Unreachable")}
} hasIcon={true}
}} />
> </Box>
{isCheckingConnection ? <Box mt={theme.spacing(2)}>
t("retryingConnection", "Retrying Connection...") : <Typography
t("retryConnection", "Retry Connection")} variant="body1"
</Button> align="center"
</Box> color={theme.palette.primary.contrastTextSecondary}
</Stack> >
</Stack> {t(
</Stack> "backendUnreachableMessage",
); "The Checkmate server is not responding. Please check your deployment configuration or try again later."
)}
</Typography>
</Box>
<Box sx={{ mt: theme.spacing(4) }}>
<Button
variant="contained"
color="accent"
onClick={handleRetry}
disabled={isCheckingConnection}
className="dashboard-style-button"
sx={{
px: theme.spacing(6),
borderRadius: `${theme.shape.borderRadius}px !important`,
"&.MuiButtonBase-root": {
borderRadius: `${theme.shape.borderRadius}px !important`,
},
"&.MuiButton-root": {
borderRadius: `${theme.shape.borderRadius}px !important`,
},
}}
>
{isCheckingConnection
? t("retryingConnection", "Retrying Connection...")
: t("retryConnection", "Retry Connection")}
</Button>
</Box>
</Stack>
</Stack>
</Stack>
);
}; };
export default ServerUnreachable; export default ServerUnreachable;
+6 -1
View File
@@ -12,7 +12,12 @@ const SettingsAbout = () => {
return ( return (
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h1">{t("settingsAbout")}</Typography> <Typography
component="h1"
variant="h2"
>
{t("settingsAbout")}
</Typography>
</Box> </Box>
<Box> <Box>
<Typography component="h2">Checkmate {2.1}</Typography> <Typography component="h2">Checkmate {2.1}</Typography>
@@ -23,7 +23,12 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) =
<> <>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h1">{t("settingsDemoMonitors")}</Typography> <Typography
component="h1"
variant="h2"
>
{t("settingsDemoMonitors")}
</Typography>
<Typography sx={HEADER_SX}>{t("settingsDemoMonitorsDescription")}</Typography> <Typography sx={HEADER_SX}>{t("settingsDemoMonitorsDescription")}</Typography>
</Box> </Box>
<Box> <Box>
@@ -48,7 +53,12 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) =
</ConfigBox> </ConfigBox>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h1">{t("settingsSystemReset")}</Typography> <Typography
component="h1"
variant="h2"
>
{t("settingsSystemReset")}
</Typography>
<Typography sx={{ mt: theme.spacing(2) }}> <Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsSystemResetDescription")} {t("settingsSystemResetDescription")}
</Typography> </Typography>
+46 -4
View File
@@ -11,6 +11,7 @@ import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments"; import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
import { useSendTestEmail } from "../../Hooks/useSendTestEmail"; import { useSendTestEmail } from "../../Hooks/useSendTestEmail";
import { createToast } from "../../Utils/toastUtils";
const SettingsEmail = ({ const SettingsEmail = ({
isAdmin, isAdmin,
@@ -29,7 +30,7 @@ const SettingsEmail = ({
const [hasBeenReset, setHasBeenReset] = useState(false); const [hasBeenReset, setHasBeenReset] = useState(false);
// Network // Network
const [isSending, error, sendTestEmail] = useSendTestEmail(); const [isSending, , sendTestEmail] = useSendTestEmail(); // Using empty placeholder for unused error variable
// Handlers // Handlers
const handlePasswordChange = (e) => { const handlePasswordChange = (e) => {
@@ -40,6 +41,33 @@ const SettingsEmail = ({
}); });
}; };
/**
* Handle sending test email with current form values
*/
const handleSendTestEmail = () => {
// Collect current form values
const emailConfig = {
systemEmailHost: settingsData?.settings?.systemEmailHost,
systemEmailPort: settingsData?.settings?.systemEmailPort,
systemEmailUser: settingsData?.settings?.systemEmailUser,
systemEmailAddress: settingsData?.settings?.systemEmailAddress,
systemEmailPassword: password || settingsData?.settings?.systemEmailPassword,
systemEmailConnectionHost: settingsData?.settings?.systemEmailConnectionHost,
};
// Basic validation
if (!emailConfig.systemEmailHost || !emailConfig.systemEmailPort) {
createToast({
body: t("settingsEmailRequiredFields", "Email host and port are required"),
variant: "error",
});
return;
}
// Send test email with current form values
sendTestEmail(emailConfig);
};
if (!isAdmin) { if (!isAdmin) {
return null; return null;
} }
@@ -47,7 +75,12 @@ const SettingsEmail = ({
return ( return (
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h1">{t("settingsEmail")}</Typography> <Typography
component="h1"
variant="h2"
>
{t("settingsEmail")}
</Typography>
<Typography sx={HEADER_SX}>{t("settingsEmailDescription")}</Typography> <Typography sx={HEADER_SX}>{t("settingsEmailDescription")}</Typography>
</Box> </Box>
<Box> <Box>
@@ -122,14 +155,23 @@ const SettingsEmail = ({
</Button> </Button>
</Box> </Box>
)} )}
<Box>
<Typography>{t("settingsEmailConnectionHost")}</Typography>
<TextInput
name="systemEmailConnectionHost"
placeholder="bluewavelabs.ca"
value={settingsData?.settings?.systemEmailConnectionHost ?? ""}
onChange={handleChange}
/>
</Box>
<Box> <Box>
<Button <Button
variant="contained" variant="contained"
color="accent" color="accent"
loading={isSending} loading={isSending}
onClick={sendTestEmail} onClick={handleSendTestEmail}
> >
Send test e-mail {t("settingsTestEmail", "Send test e-mail")}
</Button> </Button>
</Box> </Box>
</Stack> </Stack>
@@ -41,7 +41,12 @@ const SettingsPagespeed = ({
return ( return (
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h1">{t("pageSpeedApiKeyFieldTitle")}</Typography> <Typography
component="h1"
variant="h2"
>
{t("pageSpeedApiKeyFieldTitle")}
</Typography>
<Typography sx={HEADING_SX}>{t("pageSpeedApiKeyFieldDescription")}</Typography> <Typography sx={HEADING_SX}>{t("pageSpeedApiKeyFieldDescription")}</Typography>
</Box> </Box>
<Stack gap={theme.spacing(20)}> <Stack gap={theme.spacing(20)}>
@@ -26,6 +26,7 @@ const SettingsStats = ({ isAdmin, HEADING_SX, handleChange, settingsData, errors
<Box> <Box>
<Typography <Typography
component="h1" component="h1"
variant="h2"
sx={HEADING_SX} sx={HEADING_SX}
> >
{t("settingsHistoryAndMonitoring")} {t("settingsHistoryAndMonitoring")}
@@ -15,7 +15,12 @@ const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => {
return ( return (
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h1">{t("settingsGeneralSettings")}</Typography> <Typography
component="h1"
variant="h2"
>
{t("settingsGeneralSettings")}
</Typography>
<Typography sx={HEADING_SX}> <Typography sx={HEADING_SX}>
<Typography component="span">{t("settingsDisplayTimezone")}</Typography>-{" "} <Typography component="span">{t("settingsDisplayTimezone")}</Typography>-{" "}
{t("settingsDisplayTimezoneDescription")} {t("settingsDisplayTimezoneDescription")}
+6 -1
View File
@@ -16,7 +16,12 @@ const SettingsUI = ({ HEADING_SX, handleChange, mode, language }) => {
return ( return (
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h1">{t("settingsAppearance")}</Typography> <Typography
component="h1"
variant="h2"
>
{t("settingsAppearance")}
</Typography>
<Typography sx={HEADING_SX}>{t("settingsAppearanceDescription")}</Typography> <Typography sx={HEADING_SX}>{t("settingsAppearanceDescription")}</Typography>
</Box> </Box>
<Stack gap={theme.spacing(20)}> <Stack gap={theme.spacing(20)}>
+43
View File
@@ -0,0 +1,43 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import ConfigBox from "../../Components/ConfigBox";
import Select from "../../Components/Inputs/Select";
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useTranslation } from "react-i18next";
const SettingsURL = ({ HEADING_SX, handleChange, showURL }) => {
const { t } = useTranslation();
const theme = useTheme();
return (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsURLTitle")}</Typography>
<Typography sx={HEADING_SX}>{t("settingsURLDescription")}</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Select
name="showURL"
label={t("settingsURLSelectTitle")}
value={showURL}
onChange={handleChange}
items={[
{ _id: true, name: t("settingsURLEnabled") },
{ _id: false, name: t("settingURLDisabled") },
]}
></Select>
</Stack>
</ConfigBox>
);
};
SettingsURL.propTypes = {
HEADING_SX: PropTypes.object,
handleChange: PropTypes.func,
showURL: PropTypes.bool,
};
export default SettingsURL;
+14 -2
View File
@@ -3,6 +3,7 @@ import Typography from "@mui/material/Typography";
import Breadcrumbs from "../../Components/Breadcrumbs"; import Breadcrumbs from "../../Components/Breadcrumbs";
import SettingsTimeZone from "./SettingsTimeZone"; import SettingsTimeZone from "./SettingsTimeZone";
import SettingsUI from "./SettingsUI"; import SettingsUI from "./SettingsUI";
import SettingsURL from "./SettingsURL";
import SettingsPagespeed from "./SettingsPagespeed"; import SettingsPagespeed from "./SettingsPagespeed";
import SettingsDemoMonitors from "./SettingsDemoMonitors"; import SettingsDemoMonitors from "./SettingsDemoMonitors";
import SettingsAbout from "./SettingsAbout"; import SettingsAbout from "./SettingsAbout";
@@ -15,7 +16,7 @@ import { useState } from "react";
import { useTheme } from "@emotion/react"; import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useSelector, useDispatch } from "react-redux"; import { useSelector, useDispatch } from "react-redux";
import { setTimezone, setMode, setLanguage } from "../../Features/UI/uiSlice"; import { setTimezone, setMode, setLanguage, setShowURL } from "../../Features/UI/uiSlice";
import SettingsStats from "./SettingsStats"; import SettingsStats from "./SettingsStats";
import { import {
deleteMonitorChecksByTeamId, deleteMonitorChecksByTeamId,
@@ -31,7 +32,7 @@ const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }];
const Settings = () => { const Settings = () => {
// Redux state // Redux state
const { mode, language, timezone } = useSelector((state) => state.ui); const { mode, language, timezone, showURL } = useSelector((state) => state.ui);
const { user } = useSelector((state) => state.auth); const { user } = useSelector((state) => state.auth);
// Local state // Local state
@@ -57,6 +58,11 @@ const Settings = () => {
const handleChange = async (e) => { const handleChange = async (e) => {
const { name, value } = e.target; const { name, value } = e.target;
// Special case for showURL until handled properly in the backend
if (name === "showURL") {
dispatch(setShowURL(value));
return;
}
// Build next state early // Build next state early
const newSettingsData = { const newSettingsData = {
...settingsData, ...settingsData,
@@ -127,6 +133,7 @@ const Settings = () => {
}; };
const handleSave = () => { const handleSave = () => {
console.log(settingsData.settings);
const { error } = settingsValidation.validate(settingsData.settings, { const { error } = settingsValidation.validate(settingsData.settings, {
abortEarly: false, abortEarly: false,
}); });
@@ -164,6 +171,11 @@ const Settings = () => {
setSettingsData={setSettingsData} setSettingsData={setSettingsData}
isApiKeySet={settingsData?.pagespeedKeySet ?? false} isApiKeySet={settingsData?.pagespeedKeySet ?? false}
/> />
<SettingsURL
HEADING_SX={HEADING_SX}
handleChange={handleChange}
showURL={showURL}
/>
<SettingsStats <SettingsStats
isAdmin={isAdmin} isAdmin={isAdmin}
HEADING_SX={HEADING_SX} HEADING_SX={HEADING_SX}
@@ -9,7 +9,12 @@ const ConfigStack = ({ title, description, children }) => {
return ( return (
<ConfigBox> <ConfigBox>
<Stack gap={theme.spacing(6)}> <Stack gap={theme.spacing(6)}>
<Typography component="h2">{title}</Typography> <Typography
component="h2"
variant="h2"
>
{title}
</Typography>
<Typography component="p">{description}</Typography> <Typography component="p">{description}</Typography>
</Stack> </Stack>
{children} {children}
@@ -34,10 +34,13 @@ const TabSettings = ({
<Stack gap={theme.spacing(10)}> <Stack gap={theme.spacing(10)}>
<ConfigBox> <ConfigBox>
<Stack> <Stack>
<Typography component="h2">{t("access")}</Typography> <Typography
<Typography component="p"> component="h2"
{t("statusPageCreateSettings")} variant="h2"
>
{t("access")}
</Typography> </Typography>
<Typography component="p">{t("statusPageCreateSettings")}</Typography>
</Stack> </Stack>
<Stack gap={theme.spacing(18)}> <Stack gap={theme.spacing(18)}>
<Checkbox <Checkbox
@@ -51,7 +54,12 @@ const TabSettings = ({
</ConfigBox> </ConfigBox>
<ConfigBox> <ConfigBox>
<Stack gap={theme.spacing(6)}> <Stack gap={theme.spacing(6)}>
<Typography component="h2">{t("basicInformation")}</Typography> <Typography
component="h2"
variant="h2"
>
{t("basicInformation")}
</Typography>
<Typography component="p"> <Typography component="p">
{t("statusPageCreateBasicInfoDescription")} {t("statusPageCreateBasicInfoDescription")}
</Typography> </Typography>
@@ -82,7 +90,12 @@ const TabSettings = ({
</ConfigBox> </ConfigBox>
<ConfigBox> <ConfigBox>
<Stack gap={theme.spacing(6)}> <Stack gap={theme.spacing(6)}>
<Typography component="h2">{t("timezone")}</Typography> <Typography
component="h2"
variant="h2"
>
{t("timezone")}
</Typography>
<Typography component="p"> <Typography component="p">
{t("statusPageCreateSelectTimeZoneDescription")} {t("statusPageCreateSelectTimeZoneDescription")}
</Typography> </Typography>
@@ -100,7 +113,12 @@ const TabSettings = ({
</ConfigBox> </ConfigBox>
<ConfigBox> <ConfigBox>
<Stack gap={theme.spacing(6)}> <Stack gap={theme.spacing(6)}>
<Typography component="h2">{t("settingsAppearance")}</Typography> <Typography
component="h2"
variant="h2"
>
{t("settingsAppearance")}
</Typography>
<Typography component="p"> <Typography component="p">
{t("statusPageCreateAppearanceDescription")} {t("statusPageCreateAppearanceDescription")}
</Typography> </Typography>
@@ -23,7 +23,6 @@ const Tabs = ({
setTab, setTab,
TAB_LIST, TAB_LIST,
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
return ( return (
<TabContext value={TAB_LIST[tab]}> <TabContext value={TAB_LIST[tab]}>
@@ -63,7 +62,6 @@ const Tabs = ({
setSelectedMonitors={setSelectedMonitors} setSelectedMonitors={setSelectedMonitors}
/> />
)} )}
</TabContext> </TabContext>
); );
}; };
@@ -15,7 +15,7 @@ const useMonitorsFetch = () => {
const response = await networkService.getMonitorsByTeamId({ const response = await networkService.getMonitorsByTeamId({
teamId: user.teamId, teamId: user.teamId,
limit: null, // donot return any checks for the monitors limit: null, // donot return any checks for the monitors
types: ["http"], // status page is available only for the uptime type types: ["http", "ping"], // status page is available for uptime and ping monitors
}); });
setMonitors(response.data.data.monitors); setMonitors(response.data.data.monitors);
} catch (error) { } catch (error) {
+2 -1
View File
@@ -184,7 +184,6 @@ const CreateStatusPage = () => {
size: null, size: null,
}; };
} }
setForm((prev) => { setForm((prev) => {
return { return {
...prev, ...prev,
@@ -194,6 +193,8 @@ const CreateStatusPage = () => {
monitors: statusPageMonitors.map((monitor) => monitor._id), monitors: statusPageMonitors.map((monitor) => monitor._id),
color: statusPage?.color, color: statusPage?.color,
logo: newLogo, logo: newLogo,
showCharts: statusPage?.showCharts ?? true,
showUptimePercentage: statusPage?.showUptimePercentage ?? true,
}; };
}); });
setSelectedMonitors(statusPageMonitors); setSelectedMonitors(statusPageMonitors);
@@ -8,9 +8,15 @@ import { StatusLabel } from "../../../../../Components/Label";
import { useTheme } from "@mui/material/styles"; import { useTheme } from "@mui/material/styles";
import useUtils from "../../../../Uptime/Monitors/Hooks/useUtils"; import useUtils from "../../../../Uptime/Monitors/Hooks/useUtils";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { useSelector } from "react-redux";
const MonitorsList = ({ isLoading = false, shouldRender = true, monitors = [] }) => { const MonitorsList = ({ isLoading = false, shouldRender = true, monitors = [] }) => {
const theme = useTheme(); const theme = useTheme();
const { determineState } = useUtils(); const { determineState } = useUtils();
const { showURL } = useSelector((state) => state.ui);
return ( return (
<> <>
{monitors?.map((monitor) => { {monitors?.map((monitor) => {
@@ -27,6 +33,7 @@ const MonitorsList = ({ isLoading = false, shouldRender = true, monitors = [] })
title={monitor.name} title={monitor.name}
percentageColor={monitor.percentageColor} percentageColor={monitor.percentageColor}
percentage={monitor.percentage} percentage={monitor.percentage}
showURL={showURL}
/> />
<Stack <Stack
direction="row" direction="row"
+2 -1
View File
@@ -19,6 +19,7 @@ import { useState } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const PublicStatus = () => { const PublicStatus = () => {
const { url } = useParams(); const { url } = useParams();
@@ -33,7 +34,7 @@ const PublicStatus = () => {
const [statusPage, monitors, isLoading, networkError, fetchStatusPage] = const [statusPage, monitors, isLoading, networkError, fetchStatusPage] =
useStatusPageFetch(false, url); useStatusPageFetch(false, url);
const [deleteStatusPage, isDeleting] = useStatusPageDelete(fetchStatusPage, url); const [deleteStatusPage, isDeleting] = useStatusPageDelete(fetchStatusPage, url);
// Breadcrumbs // Breadcrumbs
const crumbs = [ const crumbs = [
{ name: t("statusBreadCrumbsStatusPages"), path: "/status" }, { name: t("statusBreadCrumbsStatusPages"), path: "/status" },
@@ -4,7 +4,8 @@ import { Button, Typography } from "@mui/material";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
const UploadFile = ({ onFileSelect }) => { // Changed prop to onFileSelect const UploadFile = ({ onFileSelect }) => {
// Changed prop to onFileSelect
const theme = useTheme(); const theme = useTheme();
const [file, setFile] = useState(); const [file, setFile] = useState();
const [error, setError] = useState(""); const [error, setError] = useState("");
@@ -18,17 +19,17 @@ const UploadFile = ({ onFileSelect }) => { // Changed prop to onFileSelect
const handleFileChange = (e) => { const handleFileChange = (e) => {
setError(""); setError("");
const selectedFile = e.target.files[0]; const selectedFile = e.target.files[0];
// Basic file validation // Basic file validation
if (!selectedFile) return; if (!selectedFile) return;
if (!selectedFile.name.endsWith('.csv')) { if (!selectedFile.name.endsWith(".csv")) {
setError(t("bulkImport.invalidFileType")); setError(t("bulkImport.invalidFileType"));
return; return;
} }
setFile(selectedFile); setFile(selectedFile);
onFileSelect(selectedFile); // Pass the file directly to parent onFileSelect(selectedFile); // Pass the file directly to parent
}; };
return ( return (
@@ -63,11 +64,10 @@ const UploadFile = ({ onFileSelect }) => { // Changed prop to onFileSelect
</Button> </Button>
</div> </div>
); );
} };
UploadFile.prototype = { UploadFile.prototype = {
onFileSelect: PropTypes.func.isRequired, onFileSelect: PropTypes.func.isRequired,
}; };
export default UploadFile; export default UploadFile;
+6 -1
View File
@@ -61,7 +61,12 @@ const BulkImport = () => {
</Typography> </Typography>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h2">{t("bulkImport.selectFileTips")}</Typography> <Typography
component="h2"
variant="h2"
>
{t("bulkImport.selectFileTips")}
</Typography>
<Typography component="p"> <Typography component="p">
<Trans <Trans
i18nKey="bulkImport.selectFileDescription" i18nKey="bulkImport.selectFileDescription"
+47 -62
View File
@@ -34,6 +34,10 @@ import SkeletonLayout from "./skeleton";
import "./index.css"; import "./index.css";
import Dialog from "../../../Components/Dialog"; import Dialog from "../../../Components/Dialog";
import NotificationIntegrationModal from "../../../Components/NotificationIntegrationModal/Components/NotificationIntegrationModal"; import NotificationIntegrationModal from "../../../Components/NotificationIntegrationModal/Components/NotificationIntegrationModal";
import { usePauseMonitor } from "../../../Hooks/useMonitorControls";
import PauseOutlinedIcon from "@mui/icons-material/PauseOutlined";
import PlayArrowOutlinedIcon from "@mui/icons-material/PlayArrowOutlined";
import { useMonitorUtils } from "../../../Hooks/useMonitorUtils";
/** /**
* Parses a URL string and returns a URL object. * Parses a URL string and returns a URL object.
@@ -83,11 +87,19 @@ const Configure = () => {
include: "ok", include: "ok",
}; };
const [trigger, setTrigger] = useState(false);
const triggerUpdate = () => {
setTrigger(!trigger);
};
const [pauseMonitor, isPausing, error] = usePauseMonitor({
monitorId: monitor?._id,
triggerUpdate,
});
useEffect(() => { useEffect(() => {
const fetchMonitor = async () => { const fetchMonitor = async () => {
try { try {
const action = await dispatch(getUptimeMonitorById({ monitorId })); const action = await dispatch(getUptimeMonitorById({ monitorId }));
if (getUptimeMonitorById.fulfilled.match(action)) { if (getUptimeMonitorById.fulfilled.match(action)) {
const monitor = action.payload.data; const monitor = action.payload.data;
setMonitor(monitor); setMonitor(monitor);
@@ -100,7 +112,7 @@ const Configure = () => {
} }
}; };
fetchMonitor(); fetchMonitor();
}, [monitorId, navigate]); }, [monitorId, navigate, trigger]);
const handleChange = (event, name) => { const handleChange = (event, name) => {
let { checked, value, id } = event.target; let { checked, value, id } = event.target;
@@ -158,23 +170,6 @@ const Configure = () => {
} }
}; };
const handlePause = async () => {
try {
const action = await dispatch(pauseUptimeMonitor({ monitorId }));
if (pauseUptimeMonitor.fulfilled.match(action)) {
const monitor = action.payload.data;
setMonitor(monitor);
const state = action?.payload?.data.isActive === false ? "paused" : "resumed";
createToast({ body: `Monitor ${state} successfully.` });
} else if (pauseUptimeMonitor.rejected.match(action)) {
throw new Error(action.error.message);
}
} catch (error) {
logger.error("Error pausing monitor: " + monitorId);
createToast({ body: "Failed to pause monitor" });
}
};
const handleSubmit = async (event) => { const handleSubmit = async (event) => {
event.preventDefault(); event.preventDefault();
const action = await dispatch(updateUptimeMonitor({ monitor: monitor })); const action = await dispatch(updateUptimeMonitor({ monitor: monitor }));
@@ -219,17 +214,7 @@ const Configure = () => {
setIsNotificationModalOpen(false); setIsNotificationModalOpen(false);
}; };
const statusColor = { const { determineState, statusColor } = useMonitorUtils();
true: theme.palette.success.main,
false: theme.palette.error.main,
undefined: theme.palette.warning.main,
};
const statusMsg = {
true: "Your site is up.",
false: "Your site is down.",
undefined: "Pending...",
};
const { t } = useTranslation(); const { t } = useTranslation();
@@ -274,7 +259,7 @@ const Configure = () => {
gap={theme.spacing(2)} gap={theme.spacing(2)}
> >
<Tooltip <Tooltip
title={statusMsg[monitor?.status ?? undefined]} title={t(`statusMsg.${[determineState(monitor)]}`)}
disableInteractive disableInteractive
slotProps={{ slotProps={{
popper: { popper: {
@@ -290,7 +275,7 @@ const Configure = () => {
}} }}
> >
<Box> <Box>
<PulseDot color={statusColor[monitor?.status ?? undefined]} /> <PulseDot color={statusColor[determineState(monitor)]} />
</Box> </Box>
</Tooltip> </Tooltip>
<Typography <Typography
@@ -324,42 +309,26 @@ const Configure = () => {
</Stack> </Stack>
</Box> </Box>
<Box <Box
justifyContent="space-between"
sx={{ sx={{
alignSelf: "flex-end", alignSelf: "flex-end",
ml: "auto", ml: "auto",
display: "flex",
gap: theme.spacing(2),
}} }}
> >
<Button <Button
variant="contained" variant="contained"
color="secondary" color="secondary"
loading={isLoading} loading={isPausing}
sx={{ startIcon={
pl: theme.spacing(4), monitor?.isActive ? <PauseOutlinedIcon /> : <PlayArrowOutlinedIcon />
pr: theme.spacing(6), }
mr: theme.spacing(6), onClick={() => {
"& svg": { pauseMonitor();
mr: theme.spacing(2),
width: 22,
height: 22,
"& path": {
stroke: theme.palette.primary.contrastTextTertiary,
strokeWidth: 1.7,
},
},
}} }}
onClick={handlePause}
> >
{monitor?.isActive ? ( {monitor?.isActive ? t("pause") : t("resume")}
<>
<PauseIcon />
{t("pause")}
</>
) : (
<>
<ResumeIcon />
{t("resume")}
</>
)}
</Button> </Button>
<Button <Button
loading={isLoading} loading={isLoading}
@@ -374,7 +343,12 @@ const Configure = () => {
</Stack> </Stack>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h2">{t("settingsGeneralSettings")}</Typography> <Typography
component="h2"
variant="h2"
>
{t("settingsGeneralSettings")}
</Typography>
<Typography component="p"> <Typography component="p">
{t("distributedUptimeCreateSelectURL")} {t("distributedUptimeCreateSelectURL")}
</Typography> </Typography>
@@ -420,7 +394,10 @@ const Configure = () => {
</ConfigBox> </ConfigBox>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h2"> <Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateIncidentNotification")} {t("distributedUptimeCreateIncidentNotification")}
</Typography> </Typography>
<Typography component="p"> <Typography component="p">
@@ -488,7 +465,12 @@ const Configure = () => {
</ConfigBox> </ConfigBox>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h2">{t("ignoreTLSError")}</Typography> <Typography
component="h2"
variant="h2"
>
{t("ignoreTLSError")}
</Typography>
<Typography component="p">{t("ignoreTLSErrorDescription")}</Typography> <Typography component="p">{t("ignoreTLSErrorDescription")}</Typography>
</Box> </Box>
<Stack> <Stack>
@@ -508,7 +490,10 @@ const Configure = () => {
</ConfigBox> </ConfigBox>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h2"> <Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateAdvancedSettings")} {t("distributedUptimeCreateAdvancedSettings")}
</Typography> </Typography>
</Box> </Box>
+26 -5
View File
@@ -261,7 +261,12 @@ const CreateMonitor = () => {
</Typography> </Typography>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h2">{t("distributedUptimeCreateChecks")}</Typography> <Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateChecks")}
</Typography>
<Typography component="p"> <Typography component="p">
{t("distributedUptimeCreateChecksDescription")} {t("distributedUptimeCreateChecksDescription")}
</Typography> </Typography>
@@ -342,7 +347,12 @@ const CreateMonitor = () => {
</ConfigBox> </ConfigBox>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h2">{t("settingsGeneralSettings")}</Typography> <Typography
component="h2"
variant="h2"
>
{t("settingsGeneralSettings")}
</Typography>
<Typography component="p">{t("uptimeCreateSelectURL")}</Typography> <Typography component="p">{t("uptimeCreateSelectURL")}</Typography>
</Box> </Box>
<Stack gap={theme.spacing(15)}> <Stack gap={theme.spacing(15)}>
@@ -386,7 +396,10 @@ const CreateMonitor = () => {
</ConfigBox> </ConfigBox>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h2"> <Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateIncidentNotification")} {t("distributedUptimeCreateIncidentNotification")}
</Typography> </Typography>
<Typography component="p"> <Typography component="p">
@@ -417,7 +430,12 @@ const CreateMonitor = () => {
</ConfigBox> </ConfigBox>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h2">{t("ignoreTLSError")}</Typography> <Typography
component="h2"
variant="h2"
>
{t("ignoreTLSError")}
</Typography>
<Typography component="p">{t("ignoreTLSErrorDescription")}</Typography> <Typography component="p">{t("ignoreTLSErrorDescription")}</Typography>
</Box> </Box>
<Stack> <Stack>
@@ -437,7 +455,10 @@ const CreateMonitor = () => {
</ConfigBox> </ConfigBox>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography component="h2"> <Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateAdvancedSettings")} {t("distributedUptimeCreateAdvancedSettings")}
</Typography> </Typography>
</Box> </Box>
@@ -75,7 +75,7 @@ const ChartBoxes = ({
<Typography component="span"> <Typography component="span">
{hoveredUptimeData !== null {hoveredUptimeData !== null
? Math.floor(hoveredUptimeData?.originalAvgResponseTime ?? 0) ? Math.floor(hoveredUptimeData?.originalAvgResponseTime ?? 0)
: Math.floor(monitorData?.groupedUptimePercentage ?? 0 * 100)} : Math.floor((monitorData?.groupedUptimePercentage ?? 0) * 100)}
<Typography component="span"> <Typography component="span">
{hoveredUptimeData !== null ? " ms" : " %"} {hoveredUptimeData !== null ? " ms" : " %"}
</Typography> </Typography>
+12 -4
View File
@@ -1,5 +1,6 @@
// Components // Components
import Breadcrumbs from "../../../Components/Breadcrumbs"; import Breadcrumbs from "../../../Components/Breadcrumbs";
import MonitorDetailsControlHeader from "../../../Components/MonitorDetailsControlHeader";
import MonitorStatusHeader from "../../../Components/MonitorStatusHeader"; import MonitorStatusHeader from "../../../Components/MonitorStatusHeader";
import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader"; import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader";
import ChartBoxes from "./Components/ChartBoxes"; import ChartBoxes from "./Components/ChartBoxes";
@@ -36,9 +37,9 @@ const UptimeDetails = () => {
// Local state // Local state
const [dateRange, setDateRange] = useState("recent"); const [dateRange, setDateRange] = useState("recent");
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5); const [rowsPerPage, setRowsPerPage] = useState(5);
const [trigger, setTrigger] = useState(false);
// Utils // Utils
const dateFormat = const dateFormat =
@@ -52,6 +53,7 @@ const UptimeDetails = () => {
useFetchUptimeMonitorDetails({ useFetchUptimeMonitorDetails({
monitorId, monitorId,
dateRange, dateRange,
trigger,
}); });
const monitor = monitorData?.monitor; const monitor = monitorData?.monitor;
@@ -73,6 +75,10 @@ const UptimeDetails = () => {
}); });
// Handlers // Handlers
const triggerUpdate = () => {
setTrigger(!trigger);
};
const handlePageChange = (_, newPage) => { const handlePageChange = (_, newPage) => {
setPage(newPage); setPage(newPage);
}; };
@@ -101,11 +107,12 @@ const UptimeDetails = () => {
return ( return (
<Stack gap={theme.spacing(10)}> <Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} /> <Breadcrumbs list={BREADCRUMBS} />
<MonitorStatusHeader <MonitorDetailsControlHeader
path={"uptime"} path={"uptime"}
isAdmin={isAdmin} isAdmin={isAdmin}
shouldRender={!monitorIsLoading} isLoading={monitorIsLoading}
monitor={monitor} monitor={monitor}
triggerUpdate={triggerUpdate}
/> />
<GenericFallback> <GenericFallback>
<Typography>{t("distributedUptimeDetailsNoMonitorHistory")}</Typography> <Typography>{t("distributedUptimeDetailsNoMonitorHistory")}</Typography>
@@ -117,11 +124,12 @@ const UptimeDetails = () => {
return ( return (
<Stack gap={theme.spacing(10)}> <Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} /> <Breadcrumbs list={BREADCRUMBS} />
<MonitorStatusHeader <MonitorDetailsControlHeader
path={"uptime"} path={"uptime"}
isAdmin={isAdmin} isAdmin={isAdmin}
isLoading={monitorIsLoading} isLoading={monitorIsLoading}
monitor={monitor} monitor={monitor}
triggerUpdate={triggerUpdate}
/> />
<UptimeStatusBoxes <UptimeStatusBoxes
isLoading={monitorIsLoading} isLoading={monitorIsLoading}
@@ -150,7 +150,10 @@ const UptimeDataTable = ({
id: "responseTime", id: "responseTime",
content: t("responseTime"), content: t("responseTime"),
render: (row) => ( render: (row) => (
<Box display="flex" justifyContent="center"> <Box
display="flex"
justifyContent="center"
>
<BarChart checks={row.monitor.checks.slice().reverse()} /> <BarChart checks={row.monitor.checks.slice().reverse()} />
</Box> </Box>
), ),
+30 -6
View File
@@ -1047,9 +1047,12 @@ class NetworkService {
form.url && fd.append("url", form.url); form.url && fd.append("url", form.url);
form.timezone && fd.append("timezone", form.timezone); form.timezone && fd.append("timezone", form.timezone);
form.color && fd.append("color", form.color); form.color && fd.append("color", form.color);
form.showCharts && fd.append("showCharts", form.showCharts); if (form.showCharts !== undefined) {
form.showUptimePercentage && fd.append("showCharts", String(form.showCharts));
fd.append("showUptimePercentage", form.showUptimePercentage); }
if (form.showUptimePercentage !== undefined) {
fd.append("showUptimePercentage", String(form.showUptimePercentage));
}
form.monitors && form.monitors &&
form.monitors.forEach((monitorId) => { form.monitors.forEach((monitorId) => {
fd.append("monitors[]", monitorId); fd.append("monitors[]", monitorId);
@@ -1154,11 +1157,32 @@ class NetworkService {
} }
// ************************************ // ************************************
// Send tes t email // Send test email
// ************************************ // ************************************
async sendTestEmail(config) { async sendTestEmail(config) {
const { to } = config; // Extract recipient and email configuration
return this.axiosInstance.post(`/monitors/test-email`, { to }); const { to, emailConfig } = config;
// If emailConfig is provided, use the new endpoint with direct parameters
if (emailConfig) {
return this.axiosInstance.post(`/settings/test-email`, {
to,
systemEmailHost: emailConfig.systemEmailHost,
systemEmailPort: emailConfig.systemEmailPort,
systemEmailAddress: emailConfig.systemEmailAddress,
systemEmailPassword: emailConfig.systemEmailPassword,
// Only include these if they are present
...(emailConfig.systemEmailConnectionHost && {
systemEmailConnectionHost: emailConfig.systemEmailConnectionHost,
}),
...(emailConfig.systemEmailUser && {
systemEmailUser: emailConfig.systemEmailUser,
}),
});
}
// Fallback to original behavior for backward compatibility
return this.axiosInstance.post(`/settings/test-email`, { to });
} }
} }
Loaded 100 of 152 files, more files were not shown because too many files have changed in this diff. Show more