Compare commits

...
Author SHA1 Message Date
Alex Holliday f600647ff0 fix ts error 2026-02-12 19:16:16 +00:00
Alex Holliday cafea2bf5d remove unused utils, convert utils to TS 2026-02-12 19:13:53 +00:00
Alexander Holliday 4e3a82d25f Merge pull request #3276 from bluewave-labs/fix/monitor-status
fix: monitor status
2026-02-12 10:32:38 -08:00
Alexander Holliday 33e0136a2a Merge pull request #3275 from bluewave-labs/fix/ts
Fix/ts
2026-02-12 10:32:22 -08:00
Alex Holliday b0b7b6a435 fix status change issue 2026-02-12 18:30:44 +00:00
Alex Holliday 85dbe34e00 store to ts 2026-02-12 18:30:15 +00:00
Alex Holliday f38b305ba7 ui slice to TS 2026-02-12 18:07:48 +00:00
Alexander Holliday f9c32cb19b Merge pull request #3274 from bluewave-labs/fix/css-cleanup
fix: clean up index.css
2026-02-12 10:02:39 -08:00
Alex Holliday b540e48aab clean up index.css 2026-02-12 18:01:12 +00:00
Alexander Holliday a50c2578e9 Merge pull request #3273 from bluewave-labs/fix/settings
fix: put -> patch
2026-02-12 09:28:40 -08:00
Alex Holliday 2e30b622d3 put -> patch 2026-02-12 17:26:17 +00:00
Alexander Holliday 3838b18931 Merge pull request #3270 from bluewave-labs/feat/monitor-status
feat: monitor status
2026-02-11 12:30:59 -08:00
Alex Holliday 71cdd05b15 type errors 2026-02-11 20:29:50 +00:00
Alex Holliday 4b1c588f5a extract header 2026-02-11 20:11:22 +00:00
Alex Holliday cbd8b5db47 update monitor status on maintenance 2026-02-11 19:38:32 +00:00
Alex Holliday 94afe19b37 update services/repositories to reflect new monitorstatus type 2026-02-11 19:22:35 +00:00
Alex Holliday d622bce6c1 update summary type 2026-02-11 19:00:04 +00:00
Alex Holliday 7b658a9e52 initial commit 2026-02-11 18:58:12 +00:00
Alex Holliday d296aec02e add monitor statuses 2026-02-11 18:44:21 +00:00
Alexander Holliday 82c8122a5a Merge pull request #3269 from bluewave-labs/fix/cleanup
fix: cleanup
2026-02-11 10:38:27 -08:00
Alex Holliday 446c791f88 avatar text color 2026-02-11 18:36:20 +00:00
Alex Holliday e214d9a54c sidebar tweak 2026-02-11 18:34:55 +00:00
Alex Holliday 6f6920f3b2 migrate protected routes 2026-02-11 18:15:26 +00:00
Alex Holliday de447d7be2 move admin check to regsiter page 2026-02-11 17:52:26 +00:00
Alex Holliday ee3062fcb4 remove unused components 2026-02-11 17:37:50 +00:00
Alex Holliday 6443802024 cleanup imports 2026-02-11 17:21:32 +00:00
Alexander Holliday d88c73bb20 Merge pull request #3267 from bluewave-labs/feat/v2-layout
feat: v2 layout
2026-02-11 09:09:28 -08:00
Alex Holliday 10bfdc9da1 hook up offline banner 2026-02-10 23:53:06 +00:00
Alex Holliday 0503c25663 cleanup 2026-02-10 23:32:58 +00:00
Alex Holliday 442fc2ce22 account tab fix 2026-02-10 23:24:51 +00:00
Alex Holliday 71f3c90472 strings 2026-02-10 23:16:12 +00:00
Alex Holliday 4c5c2d2d5e auth footer 2026-02-10 23:12:33 +00:00
Alex Holliday a390ee5f21 auth footer 2026-02-10 22:54:23 +00:00
Alex Holliday 0d98353625 auth footer 2026-02-10 22:47:39 +00:00
Alex Holliday 0e1b074c6c styling 2026-02-10 22:28:47 +00:00
Alex Holliday a289a197a1 starprompt 2026-02-10 22:24:35 +00:00
Alex Holliday 0f6c273dad bottom menu 2026-02-10 22:17:13 +00:00
Alex Holliday 1db5f4b70f sidebar 2026-02-10 21:54:31 +00:00
Alex Holliday d1d96bd2ba sidebar 2026-02-10 19:21:57 +00:00
Alex Holliday 7ef10b4fcd initial commit 2026-02-10 17:53:00 +00:00
Alexander Holliday a2dfbd29c7 Merge pull request #3266 from bluewave-labs/fix/cleanup
fix; cleanup
2026-02-10 09:33:33 -08:00
Alex Holliday 14ba3016f6 Merge branch 'develop' into fix/cleanup 2026-02-10 17:06:29 +00:00
Alexander Holliday ac98f783e3 Merge pull request #3257 from bluewave-labs/fix/search-input-debounce
fix: changes for debounce search in uptime/infrastructure pages
2026-02-10 07:18:46 -08:00
mannilakash 98943ad461 fix: removing JSdoc for usedebounce and comments 2026-02-10 08:10:44 -05:00
Alex Holliday 28dc61e1a6 v1 cleanup 2026-02-10 00:01:27 +00:00
Alex Holliday a20fdb423a remove unused 2026-02-09 23:51:18 +00:00
Alex Holliday 1236b9191e fix email config 2026-02-09 23:41:21 +00:00
Alex Holliday 34762ef475 remove network servcie 2026-02-09 23:37:07 +00:00
Alex Holliday b817a16459 remove settings hooks 2026-02-09 23:28:46 +00:00
Alex Holliday 2091d8dcd7 remove monitorHooks 2026-02-09 23:16:14 +00:00
Alex Holliday 3d1bd94279 remove unused hooks 2026-02-09 23:02:26 +00:00
Alex Holliday 07aad0204e update slice 2026-02-09 22:56:56 +00:00
Alexander Holliday 7c0d45d287 Merge pull request #3265 from bluewave-labs/feat/v2-not-found
feat: v2 not found
2026-02-09 14:23:56 -08:00
Alex Holliday 06c571d89f img -> box 2026-02-09 22:22:01 +00:00
Alex Holliday 9b60e32125 initial commit 2026-02-09 22:19:10 +00:00
Alexander Holliday c681074bb2 Merge pull request #3264 from bluewave-labs/feat/v2-set-new-password
feat: v2 set new password
2026-02-09 14:09:39 -08:00
Alex Holliday b47e17fe91 cleanup 2026-02-09 22:07:02 +00:00
Alex Holliday 28affa1cd1 submit 2026-02-09 21:56:28 +00:00
Alex Holliday f5372b4d86 ui and strings 2026-02-09 21:29:55 +00:00
Alex Holliday 4db4947220 basic UI 2026-02-09 21:21:17 +00:00
Alex Holliday 48fb422e0f initial commit 2026-02-09 21:00:27 +00:00
Alexander Holliday 20d4f2c092 Merge pull request #3262 from bluewave-labs/feat/v2-edit-user
feat: v2 edit user
2026-02-09 12:50:17 -08:00
Alex Holliday 48081d85ee cleanup 2026-02-09 20:48:12 +00:00
Alex Holliday ece6970fa7 hook up role 2026-02-09 20:38:04 +00:00
Alex Holliday 4434189c72 remove breadcrumb 2026-02-09 20:09:08 +00:00
Alex Holliday 197e075ba1 strings 2026-02-09 20:07:45 +00:00
Alex Holliday c4ab56095e strings 2026-02-09 19:46:11 +00:00
Alex Holliday 550d5cc555 initial commit 2026-02-09 19:26:58 +00:00
Alexander Holliday b08e69764c Merge pull request #3261 from bluewave-labs/feat/v2-account
feat: v2 account
2026-02-09 11:17:25 -08:00
Alex Holliday 08e4b0c39e remove unused 2026-02-09 19:15:22 +00:00
Alex Holliday a46e282633 role column 2026-02-09 18:57:24 +00:00
Alex Holliday 8b7fd826b3 createUser endpoint 2026-02-09 18:49:31 +00:00
Alex Holliday 264e6f7d1f form submission 2026-02-09 18:29:36 +00:00
Alex Holliday fafd581f06 add team member initial commit 2026-02-09 18:14:06 +00:00
Alex Holliday d69cec9730 dialog 2026-02-09 17:52:14 +00:00
mannilakash c670653ab3 fix: format fix 2026-02-09 12:40:44 -05:00
mannilakash e75606aad3 refactor: debounce.jsx to typescript and moving it to Hooks 2026-02-09 12:38:39 -05:00
mannilakash 07b0483352 fix: changes for debounce search in uptime/infrastructure pages 2026-02-09 10:43:29 -05:00
Alex Holliday 1552018796 optional additionalButtons 2026-02-08 16:30:51 +00:00
Alex Holliday 3fc4160b88 add buttons 2026-02-08 16:23:24 +00:00
Alex Holliday 9ca79ea793 team initial commit 2026-02-08 16:14:35 +00:00
Alex Holliday 2e3b32462f remove mongo hash middleware 2026-02-07 21:15:58 +00:00
Alex Holliday 7e527b8eb2 submit 2026-02-07 21:05:45 +00:00
Alex Holliday 021b4e8ae5 controllers 2026-02-07 20:57:03 +00:00
Alex Holliday b6004241e7 validation 2026-02-07 20:54:17 +00:00
Alex Holliday af4c9d8f91 fix unset 2026-02-07 20:33:11 +00:00
Alex Holliday f2b740a8f9 put -> patch 2026-02-07 20:17:59 +00:00
Alex Holliday b26d135a4f submit 2026-02-07 20:11:30 +00:00
Alex Holliday 4f4203dbb3 remove delete 2026-02-07 19:59:28 +00:00
Alex Holliday 4945b29334 wire up form 2026-02-07 01:13:58 +00:00
Alex Holliday 1808db5cd7 form 2026-02-07 01:04:20 +00:00
Alex Holliday 31cc9cd294 fix strings 2026-02-07 00:51:04 +00:00
Alex Holliday 19b370d89b initial commit 2026-02-07 00:49:49 +00:00
Alexander Holliday 8555e7c122 Merge pull request #3254 from bluewave-labs/feat/v2-register
feat/v2 register
2026-02-06 15:39:17 -08:00
Alex Holliday 734ec41ca0 cleanup 2026-02-06 23:37:25 +00:00
Alex Holliday 5152336591 fix validation, derrive teamID and Roles from tokens 2026-02-06 23:31:07 +00:00
Alex Holliday 78b920aaf6 missing fields 2026-02-06 23:07:17 +00:00
Alex Holliday 2d84fae470 form 2026-02-06 22:53:41 +00:00
Alex Holliday 97e4b105a2 Merge branch 'develop' into feat/v2-register 2026-02-06 22:35:19 +00:00
Alex Holliday a053386f4a ui 2026-02-06 22:34:50 +00:00
Alexander Holliday e0991e28e5 Merge pull request #3253 from bluewave-labs/feat/v2/password-reset
feat: v2/password reset
2026-02-06 14:22:37 -08:00
Alex Holliday 0f26648855 remove old page 2026-02-06 22:18:44 +00:00
Alex Holliday 6a0f3426b3 recovery form, fix email service 2026-02-06 22:14:42 +00:00
Alex Holliday 16e8035299 ui 2026-02-06 21:50:45 +00:00
Alexander Holliday 31b09c316a Merge pull request #3251 from bluewave-labs/feat/v2-auth-login
feat: v2 auth login
2026-02-06 13:36:27 -08:00
Alex Holliday 8f2072e497 cleanup 2026-02-06 21:35:25 +00:00
Alex Holliday 514570da78 login page refactor 2026-02-06 21:11:46 +00:00
Alex Holliday ea68e660fc login UI 2026-02-06 20:53:48 +00:00
Alex Holliday 527543d79f base page, auth header, language and theme switches 2026-02-06 20:32:10 +00:00
Alexander Holliday 1c390a8baf Merge pull request #3250 from bluewave-labs/fix/unit-formatting
Fix/unit formatting
2026-02-06 11:43:48 -08:00
Alex Holliday 43ac5b57cf time formatting 2026-02-06 19:42:06 +00:00
Alex Holliday 1558b27497 bytes and percentage 2026-02-06 19:25:53 +00:00
Alexander Holliday 9caa6c830f Merge pull request #3249 from bluewave-labs/feat/v2-logs
Feat/v2 logs
2026-02-06 11:00:31 -08:00
Alex Holliday ea115ede53 cleanup 2026-02-06 18:56:10 +00:00
Alex Holliday 8470631162 diagnostics 2026-02-06 18:46:06 +00:00
Alex Holliday 597732e109 stats 2026-02-06 18:20:26 +00:00
Alex Holliday 9974fa4010 headers 2026-02-06 17:24:16 +00:00
Alex Holliday b0686c0576 queue tab 2026-02-05 21:40:36 +00:00
Alex Holliday a148f56b22 logs tab 2026-02-05 20:23:04 +00:00
Alexander Holliday 3b25b37c22 Merge pull request #3247 from bluewave-labs/fix/empty
Fix: empty state
2026-02-05 10:59:44 -08:00
Alexander Holliday 1e04345aa0 Merge pull request #3185 from llpingll/change-password
feat: Allow all users to change password (not just admins)
2026-02-05 10:59:20 -08:00
Alexander Holliday 174c3cd47a Merge pull request #3246 from bluewave-labs/fix/alignment
Fix/alignment
2026-02-05 10:56:59 -08:00
Alex Holliday 0e149fa477 fix empty state 2026-02-05 18:56:54 +00:00
Alex Holliday 5b4a99af3d fix select alignment 2026-02-05 18:53:51 +00:00
Alex Holliday 2ecdc2096e empty field label 2026-02-05 18:49:47 +00:00
Alexander Holliday 6e66a18535 Merge pull request #3245 from bluewave-labs/fix/pagination-delete
fix: pagination and empty state fix
2026-02-05 10:33:49 -08:00
Alex Holliday bd6aa0fa3a pagination and empty state fix 2026-02-05 18:30:43 +00:00
Alexander Holliday 4e678a4ce7 Merge pull request #3244 from bluewave-labs/feat/husky-lint
feat: add husky
2026-02-05 09:55:50 -08:00
Alex Holliday 5999315791 add gitignore 2026-02-05 17:54:06 +00:00
Alexander Holliday 0c8b8a9358 Merge pull request #3243 from bluewave-labs/feat/v2-create-maintenance
feat: v2 create maintenance
2026-02-05 09:45:20 -08:00
Alex Holliday dfe62bd301 remove old 2026-02-05 17:29:27 +00:00
Alex Holliday 747ff824cc format 2026-02-05 17:23:14 +00:00
Alex Holliday f12c544751 validation fixes 2026-02-05 17:22:24 +00:00
llpingll 278ced2a6c Removed additional lines to avoid en.json conflict 2026-02-04 23:56:16 +00:00
llpingll 88d539b2a4 Hard coded strings to avoid en.json conflicts 2026-02-04 23:32:14 +00:00
Alex Holliday f0b152fc8f duration 2026-02-04 22:55:54 +00:00
Alex Holliday dc0e198a8e monitors 2026-02-04 22:17:04 +00:00
Alex Holliday 0a5067ef46 grab monitors 2026-02-04 22:10:52 +00:00
Alex Holliday 4f0017c031 strings 2026-02-04 21:47:56 +00:00
Alex Holliday f0b2292f0a general 2026-02-04 21:45:39 +00:00
Alexander Holliday d74d79c332 Merge pull request #3242 from bluewave-labs/feat/v2-maintenance
feat: v2 maintenance
2026-02-04 12:06:22 -08:00
Alex Holliday 1a096b66f7 remove unused 2026-02-04 20:03:05 +00:00
Alex Holliday 7cb866ce4a legacy put -> patch 2026-02-04 19:57:36 +00:00
Alex Holliday c54582ca70 format 2026-02-04 19:54:01 +00:00
Alex Holliday 362034ba26 pretty-ms 2026-02-04 19:53:19 +00:00
Alex Holliday b536099d03 create header 2026-02-04 19:40:32 +00:00
Alex Holliday 04fef2ef75 put -> patch 2026-02-04 19:30:41 +00:00
Alex Holliday a89e5ed565 MaintenanceWindow table 2026-02-04 19:22:05 +00:00
Alexander Holliday ca153b65a9 Merge pull request #3241 from bluewave-labs/fix/incidents-selector
fix/incidents selector
2026-02-04 10:31:57 -08:00
Alex Holliday cc188b2ba3 Merge branch 'develop' into fix/incidents-selector 2026-02-04 18:29:49 +00:00
Alexander Holliday 18018ceef1 Merge pull request #3240 from bluewave-labs/fix/invite-permisisons
fix/invite permisisons
2026-02-04 10:13:25 -08:00
Alex Holliday 4f3bcd4624 incident page fixed 2026-02-03 23:45:23 +00:00
Alex Holliday d469590ef3 filter fix 2026-02-03 23:37:16 +00:00
llpingll 090d002578 Add comment to PasswordPanel to encourage additional error handling and remove helperText comments | Remove explicit special characters from en.json 2026-02-03 16:44:15 +00:00
llpingll 69f95723f1 Combine useNavigate and useLocation into one import 2026-02-02 22:23:48 +00:00
Lui Duarte 464b16ec9d Merge branch 'develop' into change-password 2026-02-02 15:49:30 +00:00
llpingll 04cf47544a Fix ForgotPassword format 2026-01-25 23:48:12 +00:00
Lui Duarte e1dfd98840 Merge branch 'develop' into change-password 2026-01-25 23:23:04 +00:00
llpingll 4ce4a124d6 Refactor PasswordPanel to pass navigation context via state to ForgotPassword using TextLink | Update ForgotPassword and CheckEmail to handle navigation context for Back links, ensuring users return to the correct page after password reset | Fix TextLink prop usage to avoid warnings by passing state and href correctly | Ensure error messages and navigation links use translation keys for localization 2026-01-25 22:57:36 +00:00
llpingll f564636cb4 Fix missingKey warning with helper text 2026-01-25 19:11:25 +00:00
llpingll 14a08a303b Fix helper text to correctly use translation to display message not key 2026-01-25 15:45:34 +00:00
llpingll 4645bde025 Allow all users to change password and handle expired sessions 2026-01-25 15:19:21 +00:00
335 changed files with 7193 additions and 17157 deletions

No files matched your search

+1
View File
@@ -3,3 +3,4 @@
.VSCodeCounter
*.sh
mongo
node_modules/
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
echo "Running lint-staged..."
cd client && npx lint-staged && cd ..
cd server && npx lint-staged && cd ..
+1 -1
View File
@@ -18,7 +18,7 @@
<div id="root"></div>
<script
type="module"
src="/src/main.jsx"
src="/src/main.tsx"
></script>
</body>
</html>
+577 -1
View File
@@ -26,6 +26,7 @@
"joi": "17.13.3",
"lucide-react": "^0.562.0",
"mui-color-input": "^7.0.0",
"pretty-bytes": "^7.1.0",
"pretty-ms": "^9.3.0",
"react": "18.3.1",
"react-dom": "^18.2.0",
@@ -51,6 +52,7 @@
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.6",
"lint-staged": "^16.2.7",
"prettier": "^3.3.3",
"typescript": "5.9.3",
"vite": "6.3.6"
@@ -2277,6 +2279,22 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ansi-escapes": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
"integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==",
"dev": true,
"license": "MIT",
"dependencies": {
"environment": "^1.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"dev": true,
@@ -2503,6 +2521,19 @@
"concat-map": "0.0.1"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/browserslist": {
"version": "4.28.1",
"funding": [
@@ -2628,6 +2659,39 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/cli-cursor": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
"integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
"dev": true,
"license": "MIT",
"dependencies": {
"restore-cursor": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-truncate": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz",
"integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==",
"dev": true,
"license": "MIT",
"dependencies": {
"slice-ansi": "^7.1.0",
"string-width": "^8.0.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"license": "MIT",
@@ -2651,6 +2715,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"license": "MIT",
@@ -2661,6 +2732,16 @@
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"dev": true,
@@ -3006,6 +3087,13 @@
"version": "1.5.283",
"license": "ISC"
},
"node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"dev": true,
"license": "MIT"
},
"node_modules/entities": {
"version": "4.5.0",
"license": "BSD-2-Clause",
@@ -3016,6 +3104,19 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/environment": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
"integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/error-ex": {
"version": "1.3.4",
"license": "MIT",
@@ -3512,6 +3613,19 @@
"node": "^10.12.0 || >=12.0.0"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/find-root": {
"version": "1.1.0",
"license": "MIT"
@@ -3667,6 +3781,19 @@
"node": ">=6.9.0"
}
},
"node_modules/get-east-asian-width": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
"integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"license": "MIT",
@@ -4149,6 +4276,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
"integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"get-east-asian-width": "^1.3.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-generator-function": {
"version": "1.1.2",
"dev": true,
@@ -4200,6 +4343,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/is-number-object": {
"version": "1.1.1",
"dev": true,
@@ -4478,6 +4631,56 @@
"version": "1.2.4",
"license": "MIT"
},
"node_modules/lint-staged": {
"version": "16.2.7",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz",
"integrity": "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==",
"dev": true,
"license": "MIT",
"dependencies": {
"commander": "^14.0.2",
"listr2": "^9.0.5",
"micromatch": "^4.0.8",
"nano-spawn": "^2.0.0",
"pidtree": "^0.6.0",
"string-argv": "^0.3.2",
"yaml": "^2.8.1"
},
"bin": {
"lint-staged": "bin/lint-staged.js"
},
"engines": {
"node": ">=20.17"
},
"funding": {
"url": "https://opencollective.com/lint-staged"
}
},
"node_modules/listr2": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz",
"integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"cli-truncate": "^5.0.0",
"colorette": "^2.0.20",
"eventemitter3": "^5.0.1",
"log-update": "^6.1.0",
"rfdc": "^1.4.1",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/listr2/node_modules/eventemitter3": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
"dev": true,
"license": "MIT"
},
"node_modules/locate-path": {
"version": "6.0.0",
"dev": true,
@@ -4501,6 +4704,55 @@
"dev": true,
"license": "MIT"
},
"node_modules/log-update": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
"integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-escapes": "^7.0.0",
"cli-cursor": "^5.0.0",
"slice-ansi": "^7.1.0",
"strip-ansi": "^7.1.0",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-update/node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/log-update/node_modules/strip-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/loose-envify": {
"version": "1.4.0",
"license": "MIT",
@@ -4539,6 +4791,33 @@
"node": ">= 0.4"
}
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/micromatch/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"license": "MIT",
@@ -4556,6 +4835,19 @@
"node": ">= 0.6"
}
},
"node_modules/mimic-function": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"dev": true,
@@ -4593,6 +4885,19 @@
}
}
},
"node_modules/nano-spawn": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz",
"integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20.17"
},
"funding": {
"url": "https://github.com/sindresorhus/nano-spawn?sponsor=1"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"funding": [
@@ -4731,6 +5036,22 @@
"wrappy": "1"
}
},
"node_modules/onetime": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
"integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"mimic-function": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"dev": true,
@@ -4876,6 +5197,19 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pidtree": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz",
"integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
"dev": true,
"license": "MIT",
"bin": {
"pidtree": "bin/pidtree.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"dev": true,
@@ -4932,6 +5266,18 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pretty-bytes": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-7.1.0.tgz",
"integrity": "sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==",
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pretty-ms": {
"version": "9.3.0",
"license": "MIT",
@@ -5280,6 +5626,23 @@
"node": ">=4"
}
},
"node_modules/restore-cursor": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
"integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
"dev": true,
"license": "MIT",
"dependencies": {
"onetime": "^7.0.0",
"signal-exit": "^4.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/reusify": {
"version": "1.1.0",
"dev": true,
@@ -5289,6 +5652,13 @@
"node": ">=0.10.0"
}
},
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"dev": true,
"license": "MIT"
},
"node_modules/rimraf": {
"version": "3.0.2",
"dev": true,
@@ -5565,6 +5935,49 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/slice-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
"integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"is-fullwidth-code-point": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/slice-ansi/node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/snake-case": {
"version": "3.0.4",
"license": "MIT",
@@ -5599,6 +6012,62 @@
"node": ">= 0.4"
}
},
"node_modules/string-argv": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
"integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.6.19"
}
},
"node_modules/string-width": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.1.tgz",
"integrity": "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==",
"dev": true,
"license": "MIT",
"dependencies": {
"get-east-asian-width": "^1.3.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string-width/node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/string-width/node_modules/strip-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/string.prototype.matchall": {
"version": "4.0.12",
"dev": true,
@@ -5779,6 +6248,19 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"license": "0BSD"
@@ -6175,6 +6657,84 @@
"node": ">=0.10.0"
}
},
"node_modules/wrap-ansi": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi/node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/wrap-ansi/node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/wrap-ansi/node_modules/strip-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"dev": true,
@@ -6184,6 +6744,22 @@
"version": "3.1.1",
"license": "ISC"
},
"node_modules/yaml": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
"devOptional": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"dev": true,
@@ -6203,4 +6779,4 @@
}
}
}
}
}
+5
View File
@@ -12,6 +12,9 @@
"format": "prettier --write .",
"format-check": "prettier --check ."
},
"lint-staged": {
"**/*": "npm run format"
},
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
@@ -31,6 +34,7 @@
"joi": "17.13.3",
"lucide-react": "^0.562.0",
"mui-color-input": "^7.0.0",
"pretty-bytes": "^7.1.0",
"pretty-ms": "^9.3.0",
"react": "18.3.1",
"react-dom": "^18.2.0",
@@ -56,6 +60,7 @@
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.6",
"lint-staged": "^16.2.7",
"prettier": "^3.3.3",
"typescript": "5.9.3",
"vite": "6.3.6"
+11 -17
View File
@@ -1,4 +1,4 @@
import { useEffect } from "react";
import { type CSSProperties } from "react";
import { useSelector } from "react-redux";
import "react-toastify/dist/ReactToastify.css";
import { ToastContainer } from "react-toastify";
@@ -6,21 +6,12 @@ import { ThemeProvider } from "@emotion/react";
import lightTheme from "./Utils/Theme/lightTheme";
import darkTheme from "./Utils/Theme/darkTheme";
import { CssBaseline, GlobalStyles } from "@mui/material";
import { logger } from "./Utils/Logger"; // Import the logger
import { networkService } from "./main";
import { Routes } from "./Routes";
import AppLayout from "@/Components/v1/Layouts/AppLayout";
import AppLayout from "@/Components/v2/layout/AppLayout";
import type { RootState } from "@/Types/state";
function App() {
const mode = useSelector((state) => state.ui.mode);
// Cleanup
useEffect(() => {
return () => {
logger.cleanup();
networkService.cleanup();
};
}, []);
const mode = useSelector((state: RootState) => state.ui.mode);
const theme = mode === "light" ? lightTheme : darkTheme;
@@ -30,6 +21,7 @@ function App() {
<GlobalStyles
styles={{
body: {
// @ts-ignore
backgroundColor: theme.palette.background.main,
},
}}
@@ -40,10 +32,12 @@ function App() {
<ToastContainer
newestOnTop={true}
theme={mode}
style={{
"--toastify-color-progress-light": "#7C8BA1",
"--toastify-color-progress-dark": "#7C8BA1",
}}
style={
{
"--toastify-color-progress-light": "#7C8BA1",
"--toastify-color-progress-dark": "#7C8BA1",
} as CSSProperties
}
/>
</ThemeProvider>
);
@@ -1,53 +0,0 @@
.wallet-adapter-dropdown {
width: 100%;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: var(--env-var-spacing-1);
}
.wallet-adapter-button {
display: flex;
align-items: center;
justify-content: center;
width: auto;
height: var(--env-var-height-2) !important;
font-size: var(--env-var-font-size-medium-plus) !important;
font-weight: 500 !important;
margin: 0;
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;
}
.wallet-adapter-modal-title {
font-size: var(--font-size-h5) !important;
}
/* Separator styling */
.wallet-adapter-modal-divider {
background-color: var(--border-color) !important;
margin: var(--spacing-md) 0 !important;
}
/* Responsive fixes */
@media (max-width: 1200px) {
.wallet-adapter-button {
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)
var(--env-var-spacing-1-minus) !important;
}
}
@media (max-width: 900px) {
.wallet-adapter-modal-wrapper {
flex-direction: column !important;
}
.wallet-adapter-modal-divider {
margin: var(--spacing-sm) 0 !important;
}
}
@@ -1,50 +0,0 @@
// import { useMemo } from "react";
// import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react";
// import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
// import {
// UnsafeBurnerWalletAdapter,
// PhantomWalletAdapter,
// } from "@solana/wallet-adapter-wallets";
// import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
// import { clusterApiUrl } from "@solana/web3.js";
// import PropTypes from "prop-types";
// import "./index.css";
// // Default styles that can be overridden by your app
// import "@solana/wallet-adapter-react-ui/styles.css";
// export const Wallet = ({ children }) => {
// // The network can be set to 'devnet', 'testnet', or 'mainnet-beta'.
// const network = WalletAdapterNetwork.Mainnet;
// // You can also provide a custom RPC endpoint.
// const endpoint = useMemo(() => clusterApiUrl(network), [network]);
// const wallets = useMemo(
// () => [new PhantomWalletAdapter()],
// // eslint-disable-next-line react-hooks/exhaustive-deps
// [network]
// );
// return (
// <ConnectionProvider endpoint={endpoint}>
// <WalletProvider
// wallets={wallets}
// autoConnect
// >
// <WalletModalProvider>{children}</WalletModalProvider>
// </WalletProvider>
// </ConnectionProvider>
// );
// };
// Wallet.propTypes = {
// children: PropTypes.node,
// };
const Wallet = ({ children }) => {
return children;
};
export default Wallet;
@@ -1,228 +0,0 @@
// Components
import IconButton from "@mui/material/IconButton";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import Icon from "../Icon";
import Dialog from "../Dialog/index.jsx";
// Utils
import { useState } from "react";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import { createToast } from "../../../Utils/toastUtils.jsx";
import PropTypes from "prop-types";
import { usePauseMonitor, useDeleteMonitor } from "../../../Hooks/monitorHooks.js";
const ActionsMenu = ({
monitor,
isAdmin,
updateRowCallback,
pauseCallback,
setIsLoading = () => {},
}) => {
const [anchorEl, setAnchorEl] = useState(null);
const [actions, setActions] = useState({});
const [isOpen, setIsOpen] = useState(false);
const theme = useTheme();
const [pauseMonitor, isPausing, error] = usePauseMonitor();
const [deleteMonitor, isDeleting] = useDeleteMonitor();
const handleRemove = async (event) => {
event.preventDefault();
event.stopPropagation();
let monitor = { id: actions.id };
await deleteMonitor({ monitor });
updateRowCallback();
};
const handlePause = async () => {
try {
setIsLoading(true);
await pauseMonitor({ monitorId: monitor.id });
pauseCallback();
} catch (error) {
createToast({ body: "Failed to pause monitor." });
} finally {
setIsLoading(false);
}
};
const openMenu = (event, id, url) => {
event.preventDefault();
event.stopPropagation();
setAnchorEl(event.currentTarget);
setActions({ id: id, url: url });
};
const openRemove = (e) => {
closeMenu(e);
setIsOpen(true);
};
const closeMenu = (e) => {
e.stopPropagation();
setAnchorEl(null);
};
const navigate = useNavigate();
return (
<>
<IconButton
aria-label="monitor actions"
onClick={(event) => {
event.stopPropagation();
openMenu(event, monitor.id, monitor.type === "ping" ? null : monitor.url);
}}
sx={{
"&:focus": {
outline: "none",
},
"& svg path": {
stroke: theme.palette.primary.contrastTextTertiary,
},
}}
>
<Icon
name="Settings"
size={20}
/>
</IconButton>
<Menu
className="actions-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={(e) => closeMenu(e)}
disableScrollLock
slotProps={{
paper: {
sx: {
"& ul": {
p: theme.spacing(2.5),
backgroundColor: theme.palette.primary.main,
},
"& li": { m: 0, color: theme.palette.primary.contrastTextSecondary },
/*
This should not be set automatically on the last of type
"& li:last-of-type": {
color: theme.palette.error.main,
}, */
},
},
}}
>
{actions.url !== null ? (
<MenuItem
onClick={(e) => {
closeMenu(e);
e.stopPropagation();
window.open(actions.url, "_blank", "noreferrer");
}}
>
Open site
</MenuItem>
) : (
""
)}
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/uptime/${actions.id}`);
}}
>
Details
</MenuItem>
{/* TODO - pass monitor id to Incidents page */}
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/incidents/${actions.id}`);
}}
>
Incidents
</MenuItem>
{isAdmin && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/uptime/configure/${actions.id}`);
}}
>
Configure
</MenuItem>
)}
{isAdmin && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/uptime/create/${actions.id}`);
}}
>
Clone
</MenuItem>
)}
{isAdmin && (
<MenuItem
onClick={(e) => {
closeMenu(e);
e.stopPropagation();
handlePause(e);
}}
>
{monitor?.isActive === true ? "Pause" : "Resume"}
</MenuItem>
)}
{isAdmin && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
openRemove(e);
}}
sx={{ "&.MuiButtonBase-root": { color: theme.palette.error.main } }}
>
Remove
</MenuItem>
)}
</Menu>
<Dialog
open={isOpen}
theme={theme}
title="Do you really want to delete this monitor?"
description="Once deleted, this monitor cannot be retrieved."
/* Do we need stop propagation? */
onCancel={(e) => {
e.stopPropagation();
setIsOpen(false);
}}
confirmationButtonLabel="Delete"
/* Do we need stop propagation? */
onConfirm={(e) => {
console.log(e);
e.stopPropagation();
handleRemove(e);
}}
isLoading={isDeleting}
modelTitle="modal-delete-monitor"
modelDescription="delete-monitor-confirmation"
/>
</>
);
};
ActionsMenu.propTypes = {
monitor: PropTypes.shape({
id: PropTypes.string,
url: PropTypes.string,
type: PropTypes.string,
isActive: PropTypes.bool,
}).isRequired,
isAdmin: PropTypes.bool,
updateRowCallback: PropTypes.func,
pauseCallback: PropTypes.func,
setIsLoading: PropTypes.func,
};
export default ActionsMenu;
-9
View File
@@ -1,9 +0,0 @@
.alert {
margin: 0;
width: fit-content;
}
.alert,
.alert button,
.alert .MuiTypography-root {
font-size: var(--env-var-font-size-medium);
}
-138
View File
@@ -1,138 +0,0 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Button, IconButton, Stack, Typography } from "@mui/material";
import Icon from "../Icon";
import "./index.css";
/**
* Icons mapping for different alert variants.
* @type {Object<string, JSX.Element>}
*/
const icons = {
info: (
<Icon
name="Info"
size={24}
/>
),
error: (
<Icon
name="AlertCircle"
size={24}
/>
),
warning: (
<Icon
name="AlertTriangle"
size={24}
/>
),
};
/**
* @param {Object} props
* @param {'info' | 'error' | 'warning'} props.variant - The type of alert.
* @param {string} [props.title] - The title of the alert.
* @param {string} [props.body] - The body text of the alert.
* @param {boolean} [props.isToast] - Indicates if the alert is used as a toast notification.
* @param {boolean} [props.hasIcon] - Whether to display an icon in the alert.
* @param {function} props.onClick - Toast dismiss function.
* @returns {JSX.Element}
*/
const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => {
const theme = useTheme();
/* TODO
Do we need other variants for alert?
*/
const text = theme.palette.secondary.contrastText;
const border = theme.palette.alert.contrastText;
const bg = theme.palette.alert.main;
const icon = icons[variant];
return (
<Stack
direction="row"
justifyContent="flex-start"
alignItems={hasIcon ? "" : "center"}
className="alert row-stack"
gap={theme.spacing(8)}
sx={{
padding: hasIcon ? theme.spacing(8) : `${theme.spacing(4)} ${theme.spacing(8)}`,
backgroundColor: bg,
border: `solid 1px ${border}`,
borderRadius: theme.shape.borderRadius,
}}
>
{hasIcon && <Box sx={{ color: text }}>{icon}</Box>}
<Stack
direction="column"
gap="2px"
sx={{ flex: 1 }}
>
{title && (
<Typography sx={{ fontWeight: "700", color: `${text}` }}>{title}</Typography>
)}
{body && (
<Typography sx={{ fontWeight: "400", color: `${text}` }}>{body}</Typography>
)}
{hasIcon && isToast && (
<Button
variant="text"
color="info"
onClick={onClick}
sx={{
fontWeight: "600",
width: "fit-content",
mt: theme.spacing(4),
padding: 0,
minWidth: 0,
}}
>
Dismiss
</Button>
)}
</Stack>
{isToast && (
<IconButton
onClick={onClick}
sx={{
alignSelf: "flex-start",
ml: "auto",
mr: "-5px",
mt: hasIcon ? "-5px" : 0,
padding: "5px",
"&:focus": {
outline: "none",
},
}}
>
<Icon
name="X"
size={20}
/>
</IconButton>
)}
</Stack>
);
};
Alert.propTypes = {
variant: PropTypes.oneOf(["info", "error", "warning"]).isRequired,
title: PropTypes.string,
body: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
isToast: PropTypes.bool,
hasIcon: PropTypes.bool,
onClick: function (props, propName, componentName) {
if (props.isToast && !props[propName]) {
return new Error(
`Prop '${propName}' is required when 'isToast' is true in '${componentName}'.`
);
}
return null;
},
};
export default Alert;
@@ -1,68 +0,0 @@
import PropTypes from "prop-types";
import { Box, Stack, useTheme } from "@mui/material";
// import useUtils from "../../Pages/Uptime/utils";
/**
* A component that renders a pulsating dot with a specified color.
*
* @component
* @example
* // Example usage:
* <PulseDot color="#f00" />
*
* @param {Object} props
* @param {string} props.color - The color of the dot.
* @returns {JSX.Element} The PulseDot component.
*/
const PulseDot = ({ color }) => {
const theme = useTheme();
// const { statusToTheme } = useUtils();
/* TODO refactor so it gets status and gets theme color. Then uses theme.palette.[themeColor].lowContrast */
/* const themeColor = statusToTheme[status]; */
return (
<Stack
width="26px"
height="24px"
alignItems="center"
justifyContent="center"
>
<Box
minWidth="18px"
minHeight="18px"
sx={{
position: "relative",
backgroundColor: color,
borderRadius: "50%",
"&::before": {
content: `""`,
position: "absolute",
width: "100%",
height: "100%",
backgroundColor: "inherit",
borderRadius: "50%",
animation: "ripple 1.8s ease-out infinite",
},
"&::after": {
content: `""`,
position: "absolute",
width: "7px",
height: "7px",
borderRadius: "50%",
backgroundColor: theme.palette.accent.contrastText,
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
},
}}
/>
</Stack>
);
};
PulseDot.propTypes = {
color: PropTypes.string.isRequired,
};
export default PulseDot;
@@ -1,36 +0,0 @@
import Icon from "../Icon";
import PropTypes from "prop-types";
const ArrowLeft = ({ type, color = "#667085", ...props }) => {
if (type === "double") {
return (
<Icon
name="ChevronsLeft"
color={color}
{...props}
/>
);
} else if (type === "long") {
return (
<Icon
name="ArrowLeft"
color={color}
{...props}
/>
);
} else {
return (
<Icon
name="ChevronLeft"
color={color}
{...props}
/>
);
}
};
ArrowLeft.propTypes = {
color: PropTypes.string,
type: PropTypes.oneOf(["double", "long", "default"]),
};
export default ArrowLeft;
Whitespace-only changes.
-73
View File
@@ -1,73 +0,0 @@
import { Avatar as MuiAvatar } from "@mui/material";
import PropTypes from "prop-types";
import { useSelector } from "react-redux";
import { useEffect, useState } from "react";
import { useTheme } from "@emotion/react";
/**
* @component
* @param {Object} props
* @param {string} props.src - Path to image for avatar
* @param {boolean} props.small - Specifies if avatar should be large
* @param {Object} [props.sx] - Additional styles to apply to the button.
* @returns {JSX.Element}
* @example
* // Render a red label
* <Avatar src="assets/img" first="Alex" last="Holliday" small />
*/
const Avatar = ({ src, small, sx, onClick = () => {} }) => {
const { user } = useSelector((state) => state.auth);
const theme = useTheme();
const style = small ? { width: 32, height: 32 } : { width: 64, height: 64 };
const border = small ? 1 : 3;
const [image, setImage] = useState();
useEffect(() => {
if (user.avatarImage) {
setImage(`data:image/png;base64,${user.avatarImage}`);
}
}, [user?.avatarImage]);
return (
<MuiAvatar
onClick={onClick}
alt={`${user?.firstName} ${user?.lastName}`}
/* TODO What is the /static/images/avatar/2.jpg ?*/
src={src ? src : user?.avatarImage ? image : "/static/images/avatar/2.jpg"}
sx={{
fontSize: small ? "16px" : "22px",
fontWeight: 400,
color: theme.palette.accent.contrastText,
backgroundColor: theme.palette.accent.main, // Same BG color as checkmate BG in sidebar
display: "inline-flex",
/*
TODO not sure what this is for*/
"&::before": {
content: `""`,
position: "absolute",
top: 0,
left: 0,
width: `100%`,
height: `100%`,
border: `${border}px solid rgba(255,255,255,0.2)`,
borderRadius: "50%",
},
...style,
...sx,
}}
>
{user.firstName?.charAt(0)}
{user.lastName?.charAt(0) || ""}
</MuiAvatar>
);
};
Avatar.propTypes = {
src: PropTypes.string,
small: PropTypes.bool,
sx: PropTypes.object,
onClick: PropTypes.func,
};
export default Avatar;
@@ -6,7 +6,6 @@
min-height: 16px;
}
.MuiBreadcrumbs-root .MuiBreadcrumbs-li a {
font-size: var(--env-var-font-size-medium);
font-weight: 400;
}
.MuiBreadcrumbs-root .MuiBreadcrumbs-li:not(:last-child) {
@@ -1,30 +0,0 @@
import React from "react";
import Button from "@mui/material/Button";
import { styled } from "@mui/material/styles";
const RoundGradientButton = styled(Button)(({ theme }) => ({
position: "relative",
border: "5px solid transparent",
backgroundClip: "padding-box",
borderRadius: 30,
fontSize: "1.2rem",
color: theme.palette.primary.contrastText,
backgroundColor: theme.palette.background.main,
"&:after": {
position: "absolute",
top: -3,
left: -3,
right: -3,
bottom: -3,
background:
theme.palette.mode === "dark"
? "linear-gradient(90deg, #842bd2, #ff5451, #8c52ff)"
: "linear-gradient(90deg, #842bd2, #ff5451, #8c52ff)",
content: '""',
zIndex: -1,
borderRadius: 30,
},
}));
export default RoundGradientButton;
@@ -1,214 +0,0 @@
/**
* CustomAreaChart component for rendering an area chart with optional gradient and custom ticks.
*
* @param {Object} props - The properties object.
* @param {Array} props.data - The data array for the chart.
* @param {Array} props.dataKeys - An array of data keys to be plotted as separate areas.
* @param {string} props.xKey - The key for the x-axis data.
* @param {string} [props.yKey] - The key for the y-axis data (optional).
* @param {Object} [props.xTick] - Custom tick component for the x-axis.
* @param {Object} [props.yTick] - Custom tick component for the y-axis.
* @param {string} [props.strokeColor] - The base stroke color for the areas.
* If not provided, uses a predefined color palette.
* @param {string} [props.fillColor] - The base fill color for the areas.
* @param {boolean} [props.gradient=false] - Whether to apply a gradient fill to the areas.
* @param {string} [props.gradientDirection="vertical"] - The direction of the gradient.
* @param {string} [props.gradientStartColor] - The start color of the gradient.
* Defaults to the area's stroke color if not provided.
* @param {string} [props.gradientEndColor] - The end color of the gradient.
* @param {Object} [props.customTooltip] - Custom tooltip component for the chart.
* @param {string|number} [props.height="100%"] - Height of the chart container.
*
* @returns {JSX.Element} The rendered area chart component.
*
* @example
* // Single series chart
* <CustomAreaChart
* data={temperatureData}
* dataKeys={["temperature"]}
* xKey="date"
* yKey="temperature"
* gradient={true}
* gradientStartColor="#ff6b6b"
* gradientEndColor="#4ecdc4"
* />
*
* @example
* // Multi-series chart with custom tooltip
* <CustomAreaChart
* data={performanceData}
* dataKeys={["cpu.usage", "memory.usage"]}
* xKey="timestamp"
* xTick={<CustomTimeTick />}
* yTick={<PercentageTick />}
* gradient={true}
* customTooltip={({ active, payload, label }) => (
* <CustomTooltip
* label={label}
* payload={payload}
* active={active}
* />
* )}
* />
*/
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from "recharts";
import { createGradient } from "../Utils/gradientUtils.jsx";
import PropTypes from "prop-types";
import { useTheme } from "@mui/material";
import { useId } from "react";
import { Fragment } from "react";
const CustomAreaChart = ({
data,
dataKeys,
xKey,
xDomain,
yKey,
yDomain,
xTick,
yTick,
strokeColor,
fillColor,
gradient = false,
gradientDirection = "vertical",
gradientStartColor,
gradientEndColor,
customTooltip,
height = "100%",
}) => {
const theme = useTheme();
const uniqueId = useId();
const AREA_COLORS = [
// Blues
"#3182bd", // Deep blue
"#6baed6", // Medium blue
"#9ecae1", // Light blue
// Greens
"#74c476", // Soft green
"#a1d99b", // Light green
"#c7e9c0", // Pale green
// Oranges
"#fdae6b", // Warm orange
"#fdd0a2", // Light orange
"#feedde", // Pale orange
// Purples
"#9467bd", // Lavender
"#a55194", // Deep magenta
"#c994c7", // Soft magenta
// Reds
"#ff9896", // Soft red
"#de2d26", // Deep red
"#fc9272", // Medium red
// Cyans/Teals
"#17becf", // Cyan
"#7fcdbb", // Teal
"#a1dab4", // Light teal
// Yellows
"#fec44f", // Mustard
"#fee391", // Light yellow
"#ffffd4", // Pale yellow
// Additional colors
"#e377c2", // Soft pink
"#bcbd22", // Olive
"#2ca02c", // Vibrant green
];
return (
<ResponsiveContainer
width="100%"
height={height}
// FE team HELP! Why does this overflow if set to 100%?
>
<AreaChart data={data}>
<XAxis
dataKey={xKey}
{...(xDomain && { domain: xDomain })}
{...(xTick && { tick: xTick })}
/>
<YAxis
dataKey={yKey}
{...(yDomain && { domain: yDomain })}
{...(yTick && { tick: yTick })}
/>
<CartesianGrid
stroke={theme.palette.primary.lowContrast}
strokeWidth={1}
strokeOpacity={1}
fill="transparent"
vertical={false}
/>
{dataKeys.map((dataKey, index) => {
const gradientId = `gradient-${uniqueId}-${index}`;
return (
<Fragment key={dataKey}>
{gradient === true &&
createGradient({
id: gradientId,
startColor: gradientStartColor || AREA_COLORS[index],
endColor: gradientEndColor,
direction: gradientDirection,
})}
<Area
yKey={dataKey}
key={dataKey}
type="monotone"
dataKey={dataKey}
stroke={strokeColor || AREA_COLORS[index]}
fill={gradient === true ? `url(#${gradientId})` : fillColor}
/>
</Fragment>
);
})}
{customTooltip ? (
<Tooltip
cursor={{ stroke: theme.palette.primary.lowContrast }}
content={customTooltip}
wrapperStyle={{ pointerEvents: "none" }}
/>
) : (
<Tooltip />
)}
</AreaChart>
</ResponsiveContainer>
);
};
CustomAreaChart.propTypes = {
data: PropTypes.array.isRequired,
dataKeys: PropTypes.array.isRequired,
xTick: PropTypes.object, // Recharts takes an instance of component, so we can't pass the component itself
yTick: PropTypes.object, // Recharts takes an instance of component, so we can't pass the component itself
xKey: PropTypes.string.isRequired,
xDomain: PropTypes.array,
yKey: PropTypes.string,
yDomain: PropTypes.array,
fillColor: PropTypes.string,
strokeColor: PropTypes.string,
gradient: PropTypes.bool,
gradientDirection: PropTypes.string,
gradientStartColor: PropTypes.string,
gradientEndColor: PropTypes.string,
customTooltip: PropTypes.object,
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
export default CustomAreaChart;
Whitespace-only changes.
@@ -1,181 +0,0 @@
import { useTheme } from "@emotion/react";
import { Box, Stack, Tooltip, Typography } from "@mui/material";
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
import { useEffect, useState } from "react";
import "./index.css";
import { useSelector } from "react-redux";
/* TODO add prop validation and jsdocs */
const BarChart = ({ checks = [] }) => {
const theme = useTheme();
const [animate, setAnimate] = useState(false);
const uiTimezone = useSelector((state) => state.ui.timezone);
useEffect(() => {
setAnimate(true);
});
// set responseTime to average if there's only one check
if (checks.length === 1) {
checks[0] = { ...checks[0], responseTime: 50 };
}
if (checks.length !== 25) {
const placeholders = Array(25 - checks.length).fill("placeholder");
checks = [...checks, ...placeholders];
}
return (
<Stack
direction="row"
flexWrap="nowrap"
gap={theme.spacing(1.5)}
height="50px"
width="fit-content"
onClick={(event) => event.stopPropagation()}
sx={{
cursor: "default",
}}
>
{checks.map((check, index) =>
check === "placeholder" ? (
/* TODO what is the purpose of this box? */
// CAIO_REVIEW the purpose of this box is to make sure there are always at least 25 bars
// even if there are less than 25 checks
<Box
key={`${check}-${index}`}
position="relative"
width={theme.spacing(4.5)}
height="100%"
backgroundColor={theme.palette.primary.lowContrast}
sx={{
borderRadius: theme.spacing(1.5),
}}
/>
) : (
<Tooltip
title={
<>
<Typography>
{formatDateWithTz(
check.updatedAt,
"ddd, MMMM D, YYYY, HH:mm A",
uiTimezone
)}
</Typography>
<Box mt={theme.spacing(2)}>
<Box
display="inline-block"
width={theme.spacing(4)}
height={theme.spacing(4)}
backgroundColor={
check.status
? theme.palette.success.lowContrast
: theme.palette.error.lowContrast
}
sx={{ borderRadius: "50%" }}
/>
<Stack
display="inline-flex"
direction="row"
justifyContent="space-between"
ml={theme.spacing(2)}
gap={theme.spacing(12)}
>
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
Response Time
</Typography>
<Typography component="span">
{check.originalResponseTime}
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
{" "}
ms
</Typography>
</Typography>
</Stack>
</Box>
</>
}
placement="top"
key={`check-${check?.id}`}
slotProps={{
popper: {
className: "bar-tooltip",
modifiers: [
{
name: "offset",
options: {
offset: [0, -10],
},
},
],
sx: {
"& .MuiTooltip-tooltip": {
backgroundColor: theme.palette.secondary.main,
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shape.boxShadow,
px: theme.spacing(4),
py: theme.spacing(3),
},
"& .MuiTooltip-tooltip p": {
/* TODO Font size should point to theme */
fontSize: 12,
color: theme.palette.secondary.contrastText,
fontWeight: 500,
},
"& .MuiTooltip-tooltip span": {
/* TODO Font size should point to theme */
fontSize: 11,
color: theme.palette.secondary.contrastText,
fontWeight: 600,
},
},
},
}}
>
<Box
position="relative"
width="9px"
height="100%"
backgroundColor={theme.palette.primary.lowContrast}
sx={{
borderRadius: theme.spacing(1.5),
/*
TODO this doesn't seem to be used
"&:hover > .MuiBox-root": {
filter: "brightness(0.8)",
}, */
}}
>
<Box
position="absolute"
bottom={0}
width="100%"
height={`${animate ? check.responseTime : 0}%`}
backgroundColor={
check.status
? theme.palette.success.lowContrast
: theme.palette.error.lowContrast
}
sx={{
borderRadius: theme.spacing(1.5),
transition: "height 600ms cubic-bezier(0.4, 0, 0.2, 1)",
}}
/>
</Box>
</Tooltip>
)
)}
</Stack>
);
};
export default BarChart;
@@ -1,106 +0,0 @@
// Components
import { Typography, Stack } from "@mui/material";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import IconBox from "../../IconBox/index.jsx";
/**
* `EmptyView` is a functional React component that displays an empty state view with an optional icon, header, and message.
*
* @component
* @param {Object} props - The properties that define the `EmptyView` component.
* @param {React.ReactNode} [props.icon] - An optional icon to display at the top of the empty view.
* @param {string} [props.header] - An optional header text displayed next to the icon.
* @param {string} [props.message="No Data"] - The message to be displayed in the empty view.
* @param {'h1' | 'h2' | 'h3'} [props.headingLevel="h2"] - The heading level for the message text.
* @param {string} [props.justifyContent="flex-start"] - The CSS `justify-content` value to align elements vertically.
* @param {string} [props.height="100%"] - The height of the empty view container.
*
* @example
* // Example usage of EmptyView component:
* <EmptyView
* icon={<SomeIcon />}
* header="Average Response Time"
* message="No Response Time Available"
* headingLevel="h2"
* justifyContent="center"
* height="50%"
* />
*
* @returns {React.Element} The `EmptyView` component with customizable icon, header, and message.
*/
const EmptyView = ({
icon,
header,
message = "No Data",
headingLevel = "h2",
justifyContent = "flex-start",
height = "100%",
}) => {
const theme = useTheme();
return (
<Stack
flex={1}
direction="row"
sx={{
backgroundColor: theme.palette.primary.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
borderRadius: 2,
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
}}
>
<Stack
flex={1}
alignItems="center"
sx={{
padding: theme.spacing(8),
justifyContent,
gap: theme.spacing(8),
height,
"& h2": {
color: theme.palette.primary.contrastTextSecondary,
fontSize: 15,
fontWeight: 500,
},
"& tspan, & text": {
fill: theme.palette.primary.contrastTextTertiary,
},
}}
>
<Stack
alignSelf="flex-start"
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
{icon && <IconBox>{icon}</IconBox>}
{header && <Typography component="h2">{header}</Typography>}
</Stack>
<Stack
flex={1}
justifyContent="center"
alignItems="center"
>
<Typography component={headingLevel}>{message}</Typography>
</Stack>
</Stack>
</Stack>
);
};
EmptyView.propTypes = {
message: PropTypes.string,
icon: PropTypes.node,
header: PropTypes.string,
headingLevel: PropTypes.oneOf(["h1", "h2", "h3"]),
justifyContent: PropTypes.string,
height: PropTypes.string,
};
export default EmptyView;
@@ -1,104 +0,0 @@
import { Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import IconBox from "../../IconBox/index.jsx";
import EmptyView from "./EmptyView.jsx";
import PropTypes from "prop-types";
const ChartBox = ({
children,
icon,
header,
height = "300px",
justifyContent = "space-between",
Legend,
borderRadiusRight = 4,
sx,
noDataMessage,
isEmpty = false,
}) => {
const theme = useTheme();
if (isEmpty) {
return (
<EmptyView
icon={icon}
header={header}
message={noDataMessage}
/>
);
}
return (
<Stack
flex={1}
direction="row"
sx={{
backgroundColor: theme.palette.primary.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
borderRadius: 2,
borderTopRightRadius: borderRadiusRight,
borderBottomRightRadius: borderRadiusRight,
}}
>
<Stack
flex={1}
sx={{
padding: theme.spacing(8),
justifyContent,
gap: theme.spacing(8),
height,
minWidth: 250,
"& h2": {
color: theme.palette.primary.contrastTextSecondary,
fontSize: 15,
fontWeight: 500,
},
"& .MuiBox-root:not(.area-tooltip) p": {
color: theme.palette.primary.contrastTextTertiary,
fontSize: 13,
},
"& .MuiBox-root > span": {
color: theme.palette.primary.contrastText,
fontSize: 20,
"& span": {
opacity: 0.8,
marginLeft: 2,
fontSize: 15,
},
},
"& tspan, & text": {
fill: theme.palette.primary.contrastTextTertiary,
},
"& path": {
transition: "fill 300ms ease, stroke-width 400ms ease",
},
}}
>
<Stack
alignSelf="flex-start"
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
{icon && <IconBox>{icon}</IconBox>}
{header && <Typography component="h2">{header}</Typography>}
</Stack>
{children}
</Stack>
{Legend && Legend}
</Stack>
);
};
export default ChartBox;
ChartBox.propTypes = {
children: PropTypes.node,
icon: PropTypes.node,
header: PropTypes.string,
height: PropTypes.string,
noDataMessage: PropTypes.string,
isEmpty: PropTypes.bool,
};
@@ -1,14 +0,0 @@
.radial-chart {
position: relative;
display: inline-block;
}
.radial-chart-base {
opacity: 0.3;
}
.radial-chart-progress {
transform: rotate(-90deg);
transform-origin: center;
transition: stroke-dashoffset 1.5s ease-in-out;
}
@@ -1,148 +0,0 @@
import { useTheme } from "@emotion/react";
import { useEffect, useState, useMemo } from "react";
import Stack from "@mui/material/Stack";
import { Box, Typography } from "@mui/material";
import PropTypes from "prop-types";
import "./index.css";
import CircularProgress from "@mui/material/CircularProgress";
const MINIMUM_VALUE = 0;
const MAXIMUM_VALUE = 100;
/**
* A Performant SVG based circular gauge
*
* @component
* @param {Object} props - Component properties
* @param {number} [props.progress=0] - Progress percentage (0-100)
* @param {number} [props.radius=60] - Radius of the gauge circle
* @param {number} [props.strokeWidth=15] - Width of the gauge stroke
* @param {number} [props.precision=1] - Precision of the progress percentage
* @param {string} [props.unit="%"] - Unit of progress
*
* @example
* <CustomGauge
* progress={75}
* radius={50}
* strokeWidth={10}
* />
*
* @returns {React.ReactElement} Rendered CustomGauge component
*/
const CustomGauge = ({
isLoading = false,
progress = 0,
radius = 70,
strokeWidth = 15,
precision = 1,
unit = "%",
}) => {
const theme = useTheme();
// Calculate the length of the stroke for the circle
const { circumference, totalSize, strokeLength } = useMemo(
() => ({
circumference: 2 * Math.PI * radius,
totalSize: radius * 2 + strokeWidth * 2,
strokeLength: (progress / 100) * (2 * Math.PI * radius),
}),
[radius, strokeWidth, progress]
);
// Handle initial animation
const [offset, setOffset] = useState(circumference);
useEffect(() => {
setOffset(circumference);
const timer = setTimeout(() => {
setOffset(circumference - strokeLength);
}, 100);
return () => clearTimeout(timer);
}, [progress, circumference, strokeLength]);
const progressWithinRange = Math.max(MINIMUM_VALUE, Math.min(progress, MAXIMUM_VALUE));
let fillColor;
if (progressWithinRange < 50) {
fillColor = theme.palette.success.main;
} else if (progressWithinRange < 80) {
fillColor = theme.palette.warning.lowContrast;
} else {
fillColor = theme.palette.error.lowContrast;
}
if (isLoading) {
return (
<Stack
className="radial-chart"
width={radius}
height={radius}
alignItems="center"
justifyContent="center"
>
<CircularProgress color="accent" />
</Stack>
);
}
return (
<Box
className="radial-chart"
width={radius}
height={radius}
sx={{ backgroundColor: theme.palette.primary.main, borderRadius: "50%" }}
>
<svg
viewBox={`0 0 ${totalSize} ${totalSize}`}
width={radius}
height={radius}
>
<circle
className="radial-chart-base"
stroke={theme.palette.secondary.light}
strokeWidth={strokeWidth}
fill="none"
cx={totalSize / 2} // Center the circle
cy={totalSize / 2} // Center the circle
r={radius}
/>
<circle
className="radial-chart-progress"
stroke={fillColor}
strokeWidth={strokeWidth}
strokeDasharray={`${circumference} ${circumference}`}
strokeDashoffset={offset}
strokeLinecap="round"
fill="none"
cx={totalSize / 2}
cy={totalSize / 2}
r={radius}
/>
</svg>
<Typography
className="radial-chart-text"
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
...theme.typography.h2,
fill: theme.typography.h2.color,
}}
>
{`${progressWithinRange.toFixed(precision)}${unit}`}
</Typography>
</Box>
);
};
export default CustomGauge;
CustomGauge.propTypes = {
isLoading: PropTypes.bool,
progress: PropTypes.number,
radius: PropTypes.number,
strokeWidth: PropTypes.number,
precision: PropTypes.number,
unit: PropTypes.string,
};
@@ -1,181 +0,0 @@
// Components
import { Stack, Box, Tooltip, Typography } from "@mui/material";
// Utils
import { useTheme } from "@emotion/react";
import { useState, useEffect } from "react";
import PropTypes from "prop-types";
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
import { useSelector } from "react-redux";
const PlaceholderCheck = ({ daysToShow }) => {
const theme = useTheme();
return (
<Box
width={`calc(30vw / ${daysToShow})`}
height="100%"
backgroundColor={theme.palette.primary.lowContrast}
sx={{
borderRadius: theme.spacing(1.5),
}}
/>
);
};
PlaceholderCheck.propTypes = {
daysToShow: PropTypes.number,
};
const Check = ({ check, daysToShow }) => {
const [animate, setAnimate] = useState(false);
const theme = useTheme();
useEffect(() => {
setAnimate(true);
}, []);
const uiTimezone = useSelector((state) => state.ui.timezone);
return (
<Tooltip
title={
<>
<Typography>
{formatDateWithTz(check._id, "ddd, MMMM D, YYYY", uiTimezone)}
</Typography>
<Box mt={theme.spacing(2)}>
<Stack
display="inline-flex"
direction="row"
justifyContent="space-between"
gap={theme.spacing(4)}
>
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
Uptime percentage
</Typography>
<Typography component="span">
{check.upPercentage.toFixed(2)}
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
{" "}
%
</Typography>
</Typography>
</Stack>
</Box>
</>
}
placement="top"
key={`check-${check?._id}`}
slotProps={{
popper: {
className: "bar-tooltip",
modifiers: [
{
name: "offset",
options: {
offset: [0, -10],
},
},
],
sx: {
"& .MuiTooltip-tooltip": {
backgroundColor: theme.palette.secondary.main,
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shape.boxShadow,
px: theme.spacing(4),
py: theme.spacing(3),
},
"& .MuiTooltip-tooltip p": {
/* TODO Font size should point to theme */
fontSize: 12,
color: theme.palette.secondary.contrastText,
fontWeight: 500,
},
"& .MuiTooltip-tooltip span": {
/* TODO Font size should point to theme */
fontSize: 11,
color: theme.palette.secondary.contrastText,
fontWeight: 600,
},
},
},
}}
>
<Box
position="relative"
width={`calc(30vw / ${daysToShow})`}
height="100%"
backgroundColor={theme.palette.error.lowContrast}
sx={{
borderRadius: theme.spacing(1.5),
}}
>
<Box
position="absolute"
bottom={0}
width="100%"
height={`${animate ? check.upPercentage : 0}%`}
backgroundColor={theme.palette.success.lowContrast}
sx={{
borderRadius: theme.spacing(1.5),
transition: "height 600ms cubic-bezier(0.4, 0, 0.2, 1)",
}}
/>
</Box>
</Tooltip>
);
};
Check.propTypes = {
check: PropTypes.object,
daysToShow: PropTypes.number,
};
const DePINStatusPageBarChart = ({ checks = [], daysToShow = 30 }) => {
if (checks.length !== daysToShow) {
const placeholders = Array(daysToShow - checks.length).fill("placeholder");
checks = [...checks, ...placeholders];
}
return (
<Stack
direction="row"
justifyContent="space-between"
width="100%"
flexWrap="nowrap"
height="50px"
>
{checks.map((check) => {
if (check === "placeholder") {
return (
<PlaceholderCheck
key={Math.random()}
daysToShow={daysToShow}
/>
);
}
return (
<Check
key={Math.random()}
check={check}
daysToShow={daysToShow}
/>
);
})}
</Stack>
);
};
DePINStatusPageBarChart.propTypes = {
checks: PropTypes.array,
daysToShow: PropTypes.number,
};
export default DePINStatusPageBarChart;
@@ -1,42 +0,0 @@
import { Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import IconBox from "../../IconBox/index.jsx";
import PropTypes from "prop-types";
const LegendBox = ({ children, icon, header, sx }) => {
const theme = useTheme();
return (
<Stack
direction="column"
gap={theme.spacing(4)}
borderRadius={theme.spacing(8)}
sx={{
...sx,
"& label": { pl: theme.spacing(6) },
borderLeftStyle: "solid",
borderLeftWidth: 1,
borderLeftColor: theme.palette.primary.lowContrast,
backgroundColor: theme.palette.primary.main,
padding: theme.spacing(8),
background: `linear-gradient(325deg, ${theme.palette.tertiary.main} 20%, ${theme.palette.primary.main} 45%)`,
}}
>
<Stack
direction="row"
gap={theme.spacing(6)}
>
<IconBox>{icon}</IconBox>
<Typography component="h2">{header}</Typography>
</Stack>
{children}
</Stack>
);
};
LegendBox.propTypes = {
children: PropTypes.node,
icon: PropTypes.node,
header: PropTypes.string,
};
export default LegendBox;
Whitespace-only changes.
@@ -1,222 +0,0 @@
import PropTypes from "prop-types";
import {
AreaChart,
Area,
XAxis,
Tooltip,
CartesianGrid,
ResponsiveContainer,
Text,
} from "recharts";
import { Box, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useMemo, useState } from "react";
import { useSelector } from "react-redux";
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
import {
tooltipDateFormatLookup,
tickDateFormatLookup,
} from "../Utils/chartUtilFunctions.js";
import "./index.css";
const CustomToolTip = ({ active, payload, label, dateRange }) => {
const format = tooltipDateFormatLookup(dateRange);
const uiTimezone = useSelector((state) => state.ui.timezone);
const theme = useTheme();
if (active && payload && payload.length) {
const responseTime = payload[0]?.payload?.originalAvgResponseTime
? payload[0]?.payload?.originalAvgResponseTime
: (payload[0]?.payload?.avgResponseTime ?? 0);
return (
<Box
className="area-tooltip"
sx={{
backgroundColor: theme.palette.primary.main,
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
py: theme.spacing(2),
px: theme.spacing(4),
}}
>
<Typography
sx={{
color: theme.palette.primary.contrastTextTertiary,
fontSize: 12,
fontWeight: 500,
}}
>
{formatDateWithTz(label, format, uiTimezone)}
</Typography>
<Box mt={theme.spacing(1)}>
<Box
display="inline-block"
width={theme.spacing(4)}
height={theme.spacing(4)}
backgroundColor={theme.palette.primary.main}
sx={{ borderRadius: "50%" }}
/>
<Stack
display="inline-flex"
direction="row"
justifyContent="space-between"
ml={theme.spacing(3)}
sx={{
"& span": {
color: theme.palette.primary.contrastTextTertiary,
fontSize: 11,
fontWeight: 500,
},
}}
>
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
Response time:
</Typography>
<Typography
ml={theme.spacing(4)}
component="span"
>
{Math.floor(responseTime)}
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
{" "}
ms
</Typography>
</Typography>
</Stack>
</Box>
{/* Display original value */}
</Box>
);
}
return null;
};
CustomToolTip.propTypes = {
active: PropTypes.bool,
payload: PropTypes.arrayOf(
PropTypes.shape({
value: PropTypes.number,
payload: PropTypes.shape({
_id: PropTypes.string,
avgResponseTime: PropTypes.number,
originalAvgResponseTime: PropTypes.number,
}),
})
),
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
dateRange: PropTypes.string,
};
const CustomTick = ({ x, y, payload, dateRange }) => {
const format = tickDateFormatLookup(dateRange);
const theme = useTheme();
const uiTimezone = useSelector((state) => state.ui.timezone);
return (
<Text
x={x}
y={y + 10}
textAnchor="middle"
fill={theme.palette.primary.contrastTextTertiary}
fontSize={11}
fontWeight={400}
>
{formatDateWithTz(payload?.value, format, uiTimezone)}
</Text>
);
};
CustomTick.propTypes = {
x: PropTypes.number,
y: PropTypes.number,
payload: PropTypes.object,
index: PropTypes.number,
dateRange: PropTypes.string,
};
const MonitorDetailsAreaChart = ({ checks, dateRange }) => {
const theme = useTheme();
const memoizedChecks = useMemo(() => checks, [checks[0]]);
const [isHovered, setIsHovered] = useState(false);
return (
<ResponsiveContainer
width="100%"
minWidth={25}
height={220}
>
<AreaChart
width="100%"
height="100%"
data={memoizedChecks}
margin={{
top: 10,
right: 0,
left: 0,
bottom: 0,
}}
onMouseMove={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<CartesianGrid
stroke={theme.palette.primary.lowContrast}
strokeWidth={1}
strokeOpacity={1}
fill="transparent"
vertical={false}
/>
<defs>
<linearGradient
id="colorUv"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor={theme.palette.accent.main}
stopOpacity={0.8}
/>
<stop
offset="100%"
stopColor={theme.palette.accent.light}
stopOpacity={0}
/>
</linearGradient>
</defs>
<XAxis
stroke={theme.palette.primary.lowContrast}
dataKey="_id"
tick={<CustomTick dateRange={dateRange} />}
axisLine={false}
tickLine={false}
height={20}
/>
<Tooltip
cursor={{ stroke: theme.palette.primary.lowContrast }}
content={<CustomToolTip dateRange={dateRange} />}
wrapperStyle={{ pointerEvents: "none" }}
/>
<Area
type="monotone"
dataKey="avgResponseTime"
stroke={theme.palette.accent.main} // CAIO_REVIEW
fill="url(#colorUv)"
strokeWidth={isHovered ? 2.5 : 1.5}
activeDot={{ stroke: theme.palette.accent.main, r: 5 }} // CAIO_REVIEW
/>
</AreaChart>
</ResponsiveContainer>
);
};
MonitorDetailsAreaChart.propTypes = {
checks: PropTypes.array,
dateRange: PropTypes.string,
};
export default MonitorDetailsAreaChart;
@@ -1,217 +0,0 @@
import { useTheme } from "@emotion/react";
import { Box, Stack, Tooltip, Typography } from "@mui/material";
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
import { useEffect, useState, forwardRef } from "react";
import { useSelector } from "react-redux";
import PropTypes from "prop-types";
/**
* A customizable Bar component that renders a colored bar with optional children.
* @component
*
* @param {string} width The width of the bar (e.g., "100px").
* @param {string} height The height of the bar (e.g., "50px").
* @param {string} backgroundColor The background color of the bar (e.g., "#FF5733").
* @param {string} [borderRadius] Optional border radius for the bar (e.g., "8px").
* @param {node} children The content to be rendered inside the bar.
* @returns {JSX.Element} The Bar component.
*/
const Bar = forwardRef(
({ width, height, backgroundColor, borderRadius, children, ...otherProps }, ref) => {
const theme = useTheme();
return (
<Box
ref={ref}
position="relative"
width={width}
height={height}
backgroundColor={backgroundColor}
sx={{
borderRadius: borderRadius || theme.spacing(1.5),
}}
{...otherProps}
>
{children}
</Box>
);
}
);
Bar.displayName = "Bar";
Bar.propTypes = {
width: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
height: PropTypes.string.isRequired,
backgroundColor: PropTypes.string.isRequired,
borderRadius: PropTypes.string,
children: PropTypes.node,
};
/* TODO add prop validation and jsdocs */
const StatusPageBarChart = ({ checks = [] }) => {
const theme = useTheme();
const [animate, setAnimate] = useState(false);
const uiTimezone = useSelector((state) => state.ui.timezone);
const barWidth = {
xs: "calc(60% / 25)",
xl: "calc(40% / 25)",
};
useEffect(() => {
setAnimate(true);
}, []);
// set responseTime to average if there's only one check
if (checks.length === 1) {
checks[0] = { ...checks[0], responseTime: 50 };
}
if (checks.length !== 25) {
const placeholders = Array(25 - checks.length).fill("placeholder");
checks = [...checks, ...placeholders];
}
return (
<Stack
direction="row"
justifyContent="space-between"
width="100%"
flexWrap="nowrap"
height="50px"
onClick={(event) => event.stopPropagation()}
sx={{
cursor: "default",
}}
>
{checks.map((check, index) =>
check === "placeholder" ? (
/* TODO what is the purpose of this box? */
// CAIO_REVIEW the purpose of this box is to make sure there are always at least 25 bars
// even if there are less than 25 checks
<Bar
key={`${check}-${index}`}
width={barWidth}
height="100%"
backgroundColor={theme.palette.primary.lowContrast}
/>
) : (
<Tooltip
title={
<>
<Typography>
{formatDateWithTz(
check.createdAt,
"ddd, MMMM D, YYYY, HH:mm A",
uiTimezone
)}
</Typography>
<Box mt={theme.spacing(2)}>
<Box
display="inline-block"
width={theme.spacing(4)}
height={theme.spacing(4)}
backgroundColor={
check.status
? theme.palette.success.lowContrast
: theme.palette.error.lowContrast
}
sx={{ borderRadius: "50%" }}
/>
<Stack
display="inline-flex"
direction="row"
justifyContent="space-between"
ml={theme.spacing(2)}
gap={theme.spacing(12)}
>
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
Response Time
</Typography>
<Typography component="span">
{check.originalResponseTime}
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
{" "}
ms
</Typography>
</Typography>
</Stack>
</Box>
</>
}
placement="top"
key={`check-${check?.id}`}
slotProps={{
popper: {
className: "bar-tooltip",
modifiers: [
{
name: "offset",
options: {
offset: [0, -10],
},
},
],
sx: {
"& .MuiTooltip-tooltip": {
backgroundColor: theme.palette.secondary.main,
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shape.boxShadow,
px: theme.spacing(4),
py: theme.spacing(3),
},
"& .MuiTooltip-tooltip p": {
/* TODO Font size should point to theme */
fontSize: 12,
color: theme.palette.secondary.contrastText,
fontWeight: 500,
},
"& .MuiTooltip-tooltip span": {
/* TODO Font size should point to theme */
fontSize: 11,
color: theme.palette.secondary.contrastText,
fontWeight: 600,
},
},
},
}}
>
<Bar
width={barWidth}
height="100%"
backgroundColor={theme.palette.primary.lowContrast}
>
<Box
position="absolute"
bottom={0}
width="100%"
height={`${animate ? check.responseTime : 0}%`}
backgroundColor={
check.status
? theme.palette.success.lowContrast
: theme.palette.error.lowContrast
}
sx={{
borderRadius: theme.spacing(1.5),
transition: "height 600ms cubic-bezier(0.4, 0, 0.2, 1)",
}}
/>
</Bar>
</Tooltip>
)
)}
</Stack>
);
};
export default StatusPageBarChart;
@@ -1,27 +0,0 @@
export const tooltipDateFormatLookup = (dateRange) => {
const dateFormatLookup = {
recent: "ddd. MMMM D, YYYY, hh:mm A",
day: "ddd. MMMM D, YYYY, hh:mm A",
week: "ddd. MMMM D, YYYY, hh:mm A",
month: "ddd. MMMM D, YYYY",
};
const format = dateFormatLookup[dateRange];
if (format === undefined) {
return "";
}
return format;
};
export const tickDateFormatLookup = (dateRange) => {
const tickFormatLookup = {
recent: "h:mm A",
day: "h:mm A",
week: "MM/D, h:mm A",
month: "ddd. M/D",
};
const format = tickFormatLookup[dateRange];
if (format === undefined) {
return "";
}
return format;
};
@@ -1,337 +0,0 @@
import PropTypes from "prop-types";
import { useSelector } from "react-redux";
import { useTheme } from "@mui/material";
import { Text } from "recharts";
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
import { Box, Stack, Typography } from "@mui/material";
import { tickDateFormatLookup, tooltipDateFormatLookup } from "./chartUtilFunctions.js";
/**
* Custom tick component for rendering time with timezone.
*
* @param {Object} props - The properties object.
* @param {number} props.x - The x-coordinate for the tick.
* @param {number} props.y - The y-coordinate for the tick.
* @param {Object} props.payload - The payload object containing tick data.
* @param {number} props.index - The index of the tick.
* @returns {JSX.Element} The rendered tick component.
*/
export const TzTick = ({ x, y, payload, index, dateRange }) => {
const theme = useTheme();
const uiTimezone = useSelector((state) => state.ui.timezone);
const format = tickDateFormatLookup(dateRange);
return (
<Text
x={x}
y={y + 10}
textAnchor="middle"
fill={theme.palette.primary.contrastTextTertiary}
fontSize={11}
fontWeight={400}
>
{formatDateWithTz(payload?.value, format, uiTimezone)}
</Text>
);
};
TzTick.propTypes = {
x: PropTypes.number,
y: PropTypes.number,
payload: PropTypes.object,
index: PropTypes.number,
dateRange: PropTypes.string,
};
/**
* Custom tick component for rendering percentage values.
*
* @param {Object} props - The properties object.
* @param {number} props.x - The x-coordinate for the tick.
* @param {number} props.y - The y-coordinate for the tick.
* @param {Object} props.payload - The payload object containing tick data.
* @param {number} props.index - The index of the tick.
* @returns {JSX.Element|null} The rendered tick component or null for the first tick.
*/
export const PercentTick = ({ x, y, payload, index }) => {
const theme = useTheme();
if (index === 0) return null;
return (
<Text
x={x - 20}
y={y}
textAnchor="middle"
fill={theme.palette.primary.contrastTextTertiary}
fontSize={11}
fontWeight={400}
>
{`${(payload?.value * 100).toFixed()}%`}
</Text>
);
};
PercentTick.propTypes = {
x: PropTypes.number,
y: PropTypes.number,
payload: PropTypes.object,
index: PropTypes.number,
};
/**
* Converts a decimal value to a formatted percentage string.
*
* @param {number} value - The decimal value to convert (e.g., 0.75)
* @returns {string} Formatted percentage string (e.g., "75.00%") or original input if not a number
*
* @example
* getFormattedPercentage(0.7543) // Returns "75.43%"
* getFormattedPercentage(1) // Returns "100.00%"
* getFormattedPercentage("test") // Returns "test"
*/
const getFormattedPercentage = (value) => {
if (typeof value !== "number") return value;
return `${(value * 100).toFixed(2)}%`;
};
/**
* Custom tick component for rendering network bytes per second.
*
* @param {Object} props - The properties object.
* @param {number} props.x - The x-coordinate for the tick.
* @param {number} props.y - The y-coordinate for the tick.
* @param {Object} props.payload - The payload object containing tick data.
* @param {number} props.index - The index of the tick.
* @returns {JSX.Element|null} The rendered tick component or null for the first tick.
*/
export const NetworkTick = ({ x, y, payload, index, formatter }) => {
const theme = useTheme();
if (index === 0) return null;
if (formatter === undefined) {
formatter = (value, space = false) => {
if (typeof value !== "number") return value;
// need to add space between value and unit
return `${(value / 1024).toFixed(1)}${space ? " " : ""}Kbps`;
};
}
return (
<Text
x={x - 20}
y={y}
textAnchor="middle"
fill={theme.palette.primary.contrastTextTertiary}
fontSize={11}
fontWeight={400}
>
{formatter(payload?.value, true)}
</Text>
);
};
NetworkTick.propTypes = {
x: PropTypes.number,
y: PropTypes.number,
payload: PropTypes.object,
index: PropTypes.number,
formatter: PropTypes.func,
};
/**
* Custom tooltip component for displaying infrastructure data.
*
* @param {Object} props - The properties object.
* @param {boolean} props.active - Indicates if the tooltip is active.
* @param {Array} props.payload - The payload array containing tooltip data.
* @param {string} props.label - The label for the tooltip.
* @param {string} props.yKey - The key for the y-axis data.
* @param {string} props.yLabel - The label for the y-axis data.
* @param {string} props.dotColor - The color of the dot in the tooltip.
* @returns {JSX.Element|null} The rendered tooltip component or null if inactive.
*/
export const InfrastructureTooltip = ({
active,
payload,
label,
yKey,
yIdx = -1,
yLabel,
dotColor,
dateRange,
formatter = getFormattedPercentage,
}) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
const theme = useTheme();
const format = tooltipDateFormatLookup(dateRange);
if (active && payload && payload.length) {
const [hardwareType, metric] = yKey.split(".");
return (
<Box
className="area-tooltip"
sx={{
backgroundColor: theme.palette.primary.main,
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
py: theme.spacing(2),
px: theme.spacing(4),
}}
>
<Typography
sx={{
color: theme.palette.primary.contrastTextTertiary,
fontSize: 12,
fontWeight: 500,
}}
>
{formatDateWithTz(label, format, uiTimezone)}
</Typography>
<Box mt={theme.spacing(1)}>
<Box
display="inline-block"
width={theme.spacing(4)}
height={theme.spacing(4)}
backgroundColor={dotColor}
sx={{ borderRadius: "50%" }}
/>
<Stack
display="inline-flex"
direction="row"
justifyContent="space-between"
ml={theme.spacing(3)}
sx={{
"& span": {
color: theme.palette.primary.contrastTextTertiary,
fontSize: 11,
fontWeight: 500,
},
}}
>
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
{yIdx >= 0
? `${yLabel} ${formatter(payload[0].payload[hardwareType][yIdx][metric])}`
: `${yLabel} ${formatter(payload[0].payload[yKey])}`}
</Typography>
</Stack>
</Box>
{/* Display original value */}
</Box>
);
}
return null;
};
InfrastructureTooltip.propTypes = {
active: PropTypes.bool,
payload: PropTypes.array,
label: PropTypes.oneOfType([
PropTypes.instanceOf(Date),
PropTypes.string,
PropTypes.number,
]),
yKey: PropTypes.string,
yIdx: PropTypes.number,
yLabel: PropTypes.string,
dotColor: PropTypes.string,
dateRange: PropTypes.string,
};
export const TemperatureTooltip = ({
active,
payload,
label,
keys,
dotColor,
dateRange,
}) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
const theme = useTheme();
const format = tooltipDateFormatLookup(dateRange);
const formatCoreKey = (key) => {
return key.replace(/^core(\d+)$/, "Core $1");
};
if (active && payload && payload.length) {
return (
<Box
className="area-tooltip"
sx={{
backgroundColor: theme.palette.primary.main,
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
py: theme.spacing(2),
px: theme.spacing(4),
}}
>
<Typography
sx={{
color: theme.palette.primary.contrastTextTertiary,
fontSize: 12,
fontWeight: 500,
}}
>
{formatDateWithTz(label, format, uiTimezone)}
</Typography>
<Stack direction="column">
{keys.map((key) => {
return (
<Stack
key={key}
display="inline-flex"
direction="row"
justifyContent="space-between"
ml={theme.spacing(3)}
sx={{
"& span": {
color: theme.palette.primary.contrastTextTertiary,
fontSize: 11,
fontWeight: 500,
},
}}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
>
<Box
display="inline-block"
width={theme.spacing(4)}
height={theme.spacing(4)}
backgroundColor={dotColor}
sx={{ borderRadius: "50%" }}
/>
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
{`${formatCoreKey(key)}: ${payload[0].payload[key]} °C`}
</Typography>
</Stack>
<Typography component="span"></Typography>
</Stack>
);
})}
</Stack>
</Box>
);
}
return null;
};
TemperatureTooltip.propTypes = {
active: PropTypes.bool,
keys: PropTypes.array,
payload: PropTypes.array,
label: PropTypes.oneOfType([
PropTypes.instanceOf(Date),
PropTypes.string,
PropTypes.number,
]),
dotColor: PropTypes.string,
dateRange: PropTypes.string,
};
@@ -1,47 +0,0 @@
/**
* Creates an SVG gradient definition for use in charts
* @param {Object} params - The gradient parameters
* @param {string} [params.id="colorUv"] - Unique identifier for the gradient
* @param {string} params.startColor - Starting color of the gradient (hex, rgb, or color name)
* @param {string} params.endColor - Ending color of the gradient (hex, rgb, or color name)
* @param {number} [params.startOpacity=0.8] - Starting opacity (0-1)
* @param {number} [params.endOpacity=0] - Ending opacity (0-1)
* @param {('vertical'|'horizontal')} [params.direction="vertical"] - Direction of the gradient
* @returns {JSX.Element} SVG gradient definition element
* @example
* createCustomGradient({
* startColor: "#1976D2",
* endColor: "#42A5F5",
* direction: "horizontal"
* })
*/
export const createGradient = ({
id,
startColor,
endColor,
startOpacity = 0.8,
endOpacity = 0,
direction = "vertical", // or "horizontal"
}) => (
<defs>
<linearGradient
id={id}
x1={direction === "vertical" ? "0" : "0"}
y1={direction === "vertical" ? "0" : "0"}
x2={direction === "vertical" ? "0" : "1"}
y2={direction === "vertical" ? "1" : "0"}
>
<stop
offset="0%"
stopColor={startColor}
stopOpacity={startOpacity}
/>
<stop
offset="100%"
stopColor={endColor}
stopOpacity={endOpacity}
/>
</linearGradient>
</defs>
);
-80
View File
@@ -1,80 +0,0 @@
import "./check.css";
import PropTypes from "prop-types";
import Icon from "../Icon";
import { Box, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
/**
* `Check` is a functional React component that displays a check icon and a label.
*
* @component
* @param {Object} props - The properties that define the `Check` component.
* @param {string} props.text - The text to be displayed as the label next to the check icon.
* @param {'info' | 'error' | 'success'} [props.variant='info'] - The variant of the check component, affecting its styling.
* @param {boolean} [props.outlined] - Whether the check icon should be outlined or not.
*
* @example
* // To use this component, import it and use it in your JSX like this:
* <Check text="Your Text Here" />
*
* @returns {React.Element} The `Check` component with a check icon and a label, defined by the `text` prop.
*/
const Check = ({ text, noHighlightText, variant = "info", outlined = false }) => {
const theme = useTheme();
const colors = {
success: theme.palette.success.main,
error: theme.palette.error.main,
info: theme.palette.info.border,
};
return (
<Stack
direction="row"
className="check"
gap={outlined ? theme.spacing(6) : theme.spacing(4)}
alignItems="center"
>
{outlined ? (
<Icon
name="CheckCircle"
size={20}
/>
) : (
<Icon
name="Check"
size={16}
color={colors[variant]}
/>
)}
<Typography
component="span"
sx={{
color:
variant === "info"
? theme.palette.primary.contrastTextSecondary
: colors[variant],
opacity: 0.9,
fontWeight: 450,
}}
>
{noHighlightText && (
<Typography
component="span"
sx={{ color: "inherit" }}
>
{noHighlightText}
</Typography>
)}{" "}
{text}
</Typography>
</Stack>
);
};
Check.propTypes = {
text: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
noHighlightText: PropTypes.string,
variant: PropTypes.oneOf(["info", "error", "success"]),
outlined: PropTypes.bool,
};
export default Check;
Whitespace-only changes.
@@ -1,29 +0,0 @@
import { Box } from "@mui/material";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
const CircularCount = ({ count }) => {
const theme = useTheme();
return (
<Box
component="span"
color={theme.palette.tertiary.contrastText}
border={2}
borderColor={theme.palette.accent.main}
backgroundColor={theme.palette.tertiary.main}
sx={{
padding: ".25em .75em",
borderRadius: "50rem",
fontSize: "12px",
fontWeight: 500,
}}
>
{count}
</Box>
);
};
CircularCount.propTypes = {
count: PropTypes.number,
};
export default CircularCount;
-220
View File
@@ -1,220 +0,0 @@
import { useState } from "react";
import { styled, alpha } from "@mui/material/styles";
import Box from "@mui/material/Box";
import AppBar from "@mui/material/AppBar";
import Toolbar from "@mui/material/Toolbar";
import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
import Container from "@mui/material/Container";
import MenuItem from "@mui/material/MenuItem";
import Drawer from "@mui/material/Drawer";
import Icon from "../Icon";
import ThemeSwitch from "../ThemeSwitch/index.jsx";
import { useTheme } from "@mui/material/styles";
import { useSelector } from "react-redux";
import { useLocation, useNavigate } from "react-router-dom";
const StyledToolbar = styled(Toolbar)(({ theme, mode }) => ({
display: "flex",
alignItems: "center",
justifyContent: "space-between",
flexShrink: 0,
borderRadius: `calc(${theme.shape.borderRadius}px + 4px)`,
backdropFilter: "blur(24px)",
border: "1px solid",
borderColor:
mode === "light"
? alpha(theme.palette.common.black, 0.1)
: alpha(theme.palette.common.white, 0.1),
backgroundColor:
mode === "light"
? alpha(theme.palette.common.white, 0.4)
: alpha(theme.palette.common.black, 0.4),
boxShadow: theme.shadows[3],
padding: "8px 12px",
}));
const StyledMenuItem = styled(MenuItem)(({ theme }) => ({
fontSize: "1.1rem",
margin: theme.spacing(4, 2),
}));
const AppAppBar = () => {
const [open, setOpen] = useState(false);
const theme = useTheme();
const mode = useSelector((state) => state.ui.mode);
const location = useLocation();
const navigate = useNavigate();
const logoSrc =
mode === "light" ? "/images/prism-black.png" : "/images/prism-white.png";
const toggleDrawer = (newOpen) => () => {
setOpen(newOpen);
};
const handleScroll = (id) => {
if (location.pathname === "/") {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: "smooth" });
}
} else {
navigate(`/#${id}`);
}
};
return (
<AppBar
position="fixed"
sx={{
boxShadow: 0,
bgcolor: "transparent",
backgroundImage: "none",
border: "none",
mt: "calc(var(--template-frame-height, 0px) + 28px)",
}}
>
<Container maxWidth="lg">
<StyledToolbar
variant="dense"
disableGutters
mode={mode}
>
<Box sx={{ flexGrow: 1, display: "flex", alignItems: "center", px: 0 }}>
<img
src={logoSrc}
alt="Prism Logo"
style={{
height: "auto",
width: "auto",
marginRight: "10px",
maxHeight: "32px",
}}
/>
<Box sx={{ display: { xs: "none", md: "flex" } }}>
<Button
variant="text"
color="info"
size="large"
onClick={() => handleScroll("features")}
>
Features
</Button>
<Button
variant="text"
color="info"
size="large"
onClick={() => handleScroll("highlights")}
>
Highlights
</Button>
<Button
variant="text"
color="info"
size="large"
onClick={() => handleScroll("faq")}
>
FAQ
</Button>
<Button
variant="text"
color="info"
size="large"
href="https://uprock.com/blog"
>
Blog
</Button>
</Box>
</Box>
<Box
sx={{
display: { xs: "none", md: "flex" },
gap: 1,
alignItems: "center",
}}
>
{/* <Button color="primary" variant="text" size="small">
Sign in
</Button>
<Button color="primary" variant="contained" size="small">
Sign up
</Button> */}
</Box>
<Box
sx={{
display: { xs: "flex", md: "none" },
gap: 1,
}}
>
<IconButton
aria-label="Menu button"
onClick={toggleDrawer(true)}
>
<Icon
name="Menu"
size={24}
color={theme.palette.text.primary}
/>
</IconButton>
<Drawer
anchor="top"
open={open}
onClose={toggleDrawer(false)}
PaperProps={{
sx: {
top: 0,
marginTop: 0,
borderRadius: 0,
backgroundColor: theme.palette.background.paper,
},
}}
>
<Box sx={{ p: 4, backgroundColor: theme.palette.background.main }}>
<Box
sx={{
display: "flex",
justifyContent: "flex-end",
}}
>
<IconButton onClick={toggleDrawer(false)}>
<Icon
name="X"
size={24}
color={theme.palette.text.primary}
/>
</IconButton>
</Box>
<StyledMenuItem>Features</StyledMenuItem>
<StyledMenuItem>Testimonials</StyledMenuItem>
<StyledMenuItem>Highlights</StyledMenuItem>
<StyledMenuItem>FAQ</StyledMenuItem>
<StyledMenuItem
component="a"
href="https://uprock.com/blog"
>
Blog
</StyledMenuItem>
{/* <MenuItem>
<Button color="primary" variant="contained" fullWidth>
Sign up
</Button>
</MenuItem>
<MenuItem>
<Button color="primary" variant="outlined" fullWidth>
Sign in
</Button>
</MenuItem> */}
</Box>
</Drawer>
</Box>
<ThemeSwitch />
</StyledToolbar>
</Container>
</AppBar>
);
};
export default AppAppBar;
-255
View File
@@ -1,255 +0,0 @@
import * as React from "react";
import Box from "@mui/material/Box";
import Container from "@mui/material/Container";
import IconButton from "@mui/material/IconButton";
import Link from "@mui/material/Link";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { FaFacebook, FaLinkedin, FaGithub, FaTwitter, FaEnvelope } from "react-icons/fa";
function Copyright() {
return (
<Typography sx={{ color: "text.secondary", mt: 1 }}>
{"Copyright © "}
<Link
color="text.secondary"
href="https://prism.uprock.com/"
>
UpRock
</Link>
&nbsp;
{new Date().getFullYear()}
</Typography>
);
}
export default function Footer() {
return (
<Container
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: { xs: 4, sm: 8 },
py: { xs: 24, sm: 24 },
px: { xs: 12, sm: 12 },
textAlign: { sm: "center", md: "left" },
}}
>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
pt: { xs: 4, sm: 8 },
width: "100%",
borderColor: "divider",
}}
>
<div>
<Link
color="text.secondary"
href="https://uprock.com/privacy-policy"
>
Privacy Policy
</Link>
<Typography sx={{ display: "inline", mx: 0.5, opacity: 0.5 }}>
&nbsp;•&nbsp;
</Typography>
<Link
color="text.secondary"
href="https://uprock.com/terms-of-use"
>
Terms of Service
</Link>
<Copyright />
</div>
<Stack
direction="row"
spacing={1}
useFlexGap
sx={{ justifyContent: "left", color: "text.secondary" }}
>
<IconButton
color="inherit"
size="small"
href="mailto:prism@uprock.com?subject=Interested%20in%20UpRock%20Prism"
aria-label="Contact Us"
sx={{ alignSelf: "center" }}
>
<FaEnvelope />
</IconButton>
<IconButton
color="inherit"
size="small"
href="https://facebook.com/uprockcom"
aria-label="Facebook"
sx={{ alignSelf: "center" }}
>
<FaFacebook />
</IconButton>
<IconButton
color="inherit"
size="small"
href="https://x.com/uprockcom"
aria-label="X"
sx={{ alignSelf: "center" }}
>
<FaTwitter />
</IconButton>
<IconButton
color="inherit"
size="small"
href="https://www.linkedin.com/company/uprock/"
aria-label="LinkedIn"
sx={{ alignSelf: "center" }}
>
<FaLinkedin />
</IconButton>
<IconButton
color="inherit"
size="small"
href="https://github.com/uprockcom"
aria-label="GitHub"
sx={{ alignSelf: "center" }}
>
<FaGithub />
</IconButton>
</Stack>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
mt: 4,
}}
>
<Typography
variant="h2"
sx={{ color: "text.secondary" }}
>
Made with ❤️ by&nbsp;
<Link
href="https://uprock.com"
color="inherit"
sx={{ mx: 0.5 }}
>
UpRock&nbsp;
</Link>
&&nbsp;
<Link
href="https://bluewavelabs.ca"
color="inherit"
sx={{ mx: 0.5 }}
>
Bluewave Labs
</Link>
</Typography>
<Box
sx={{
display: "flex",
alignItems: "center",
mt: 2,
}}
>
<Typography
variant="h2"
sx={{ color: "text.secondary", mr: 1 }}
>
Built on&nbsp;
</Typography>
<svg
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_);}
.st2{fill:url(#SVGID_3_);}`}
</style>
<linearGradient
id="SVGID_1_"
gradientUnits="userSpaceOnUse"
x1="360.8791"
y1="351.4553"
x2="141.213"
y2="-69.2936"
gradientTransform="matrix(1 0 0 -1 0 314)"
>
<stop
offset="0"
style={{ stopColor: "rgb(0, 255, 163)" }}
/>
<stop
offset="1"
style={{ stopColor: "rgb(220, 31, 255)" }}
/>
</linearGradient>
<path
className="st0"
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"
/>
<linearGradient
id="SVGID_2_"
gradientUnits="userSpaceOnUse"
x1="264.8291"
y1="401.6014"
x2="45.163"
y2="-19.1475"
gradientTransform="matrix(1 0 0 -1 0 314)"
>
<stop
offset="0"
style={{ stopColor: "rgb(0, 255, 163)" }}
/>
<stop
offset="1"
style={{ stopColor: "rgb(220, 31, 255)" }}
/>
</linearGradient>
<path
className="st1"
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
id="SVGID_3_"
gradientUnits="userSpaceOnUse"
x1="312.5484"
y1="376.688"
x2="92.8822"
y2="-44.061"
gradientTransform="matrix(1 0 0 -1 0 314)"
>
<stop
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,58 +0,0 @@
import { Box, Stack, Typography } from "@mui/material";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import ConfigBox from "../ConfigBox/index.jsx";
/**
* 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,
* and the right section displaying the children.
*
* @component
* @example
* return (
* <ConfigBox>
* <div>Left content (Title + Description)</div>
* <div>Right content (Children)</div>
* </ConfigBox>
* );
*/
const ConfigRow = ({ title, description, children }) => {
const theme = useTheme();
return (
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{title}
</Typography>
{description && (
<Typography
variant="body2"
mt={theme.spacing(2)}
>
{description}
</Typography>
)}
</Box>
<Stack
gap={theme.spacing(15)}
mt={theme.spacing(4)}
>
{children}
</Stack>
</ConfigBox>
);
};
ConfigRow.propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.string,
children: PropTypes.node,
};
export default ConfigRow;
-24
View File
@@ -1,24 +0,0 @@
import PropTypes from "prop-types";
const Dot = ({ color = "gray", size = "4px", style }) => {
return (
<span
style={{
content: '""',
width: size,
height: size,
borderRadius: "50%",
backgroundColor: color,
opacity: 0.8,
...style,
}}
/>
);
};
Dot.propTypes = {
color: PropTypes.string,
size: PropTypes.string,
};
export default Dot;
@@ -1,44 +0,0 @@
import { Button, Stack } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
const FallbackActionButtons = ({ link, type }) => {
const theme = useTheme();
const navigate = useNavigate();
const { t } = useTranslation();
return (
<Stack
gap={theme.spacing(10)}
alignItems="center"
>
<Button
variant="contained"
color="accent"
sx={{ fontWeight: 700 }}
onClick={() => navigate(link)}
>
{t(`${type}.fallback.actionButton`)}
</Button>
{type === "uptimeMonitor" && (
<Button
variant="contained"
color="accent"
sx={{ alignSelf: "center" }}
onClick={() => navigate("/uptime/bulk-import")}
>
{t("bulkImport.fallbackPage")}
</Button>
)}
</Stack>
);
};
FallbackActionButtons.propTypes = {
title: PropTypes.string.isRequired,
link: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
};
export default FallbackActionButtons;
@@ -1,39 +0,0 @@
import { useTheme } from "@emotion/react";
import Box from "@mui/material/Box";
import Background from "../../../assets/Images/background-grid.svg?react";
import OutputAnimation from "../../../assets/Animations/output.gif";
import DarkmodeOutput from "../../../assets/Animations/darkmodeOutput.gif";
import { useSelector } from "react-redux";
const FallbackBackground = () => {
const theme = useTheme();
const mode = useSelector((state) => state.ui.mode);
return (
<>
<Box
component="img"
src={mode === "light" ? OutputAnimation : DarkmodeOutput}
background="transparent"
alt="Loading animation"
sx={{
zIndex: 1,
border: "none",
borderRadius: theme.spacing(8),
width: "100%",
transform: "scale(0.6667)",
}}
/>
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
</>
);
};
export default FallbackBackground;
@@ -1,35 +0,0 @@
import PropTypes from "prop-types";
import { useTheme } from "@mui/material/styles";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Check from "../Check/Check.jsx";
const FallbackCheckList = ({ checks, type }) => {
const theme = useTheme();
return (
<Box
sx={{
display: "flex",
flexWrap: "wrap",
gap: theme.spacing(2),
alignItems: "flex-start",
maxWidth: { xs: "90%", md: "80%", lg: "75%" },
}}
>
{checks?.map((check, index) => (
<Check
text={check}
key={`${type.trim().split(" ")[0]}-${index}`}
outlined={true}
/>
))}
</Box>
);
};
FallbackCheckList.propTypes = {
checks: PropTypes.arrayOf(PropTypes.string).isRequired,
title: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
};
export default FallbackCheckList;
@@ -1,44 +0,0 @@
import { useTheme } from "@emotion/react";
import { Box, Stack } from "@mui/material";
import PropTypes from "prop-types";
const FallbackContainer = ({ children, type }) => {
const theme = useTheme();
return (
<Box
border={1}
borderColor={theme.palette.tertiary.border}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.tertiary.background}
overflow="hidden"
sx={{
display: "flex",
borderStyle: "dashed",
height: "fit-content",
minHeight: "60vh",
width: {
sm: "90%",
md: "70%",
lg: "50%",
xl: "40%",
},
padding: `${theme.spacing(20)} ${theme.spacing(10)}`,
}}
>
<Stack
className={`fallback__${type?.trim().split(" ")[0]}`}
alignItems="center"
gap={theme.spacing(20)}
>
{children}
</Stack>
</Box>
);
};
FallbackContainer.propTypes = {
children: PropTypes.node,
type: PropTypes.string,
};
export default FallbackContainer;
@@ -1,69 +0,0 @@
import Box from "@mui/material/Box";
import { useTheme } from "@mui/material/styles";
import Link from "@mui/material/Link";
import { Link as RouterLink } from "react-router-dom";
import { useTranslation } from "react-i18next";
import Alert from "../Alert/index.jsx";
import PropTypes from "prop-types";
const renderWarningMessage = (t) => {
return (
<>
{t("pageSpeedWarning")}{" "}
<Link
component={RouterLink}
to="/settings"
sx={{
textDecoration: "underline",
color: "inherit",
fontWeight: "inherit",
"&:hover": {
textDecoration: "underline",
},
}}
>
{t("pageSpeedLearnMoreLink")}
</Link>{" "}
{t("pageSpeedAddApiKey")}
</>
);
};
const FallbackPageSpeedWarning = ({ settingsData }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<Box sx={{ width: "80%", maxWidth: "600px", zIndex: 1 }}>
<Box
sx={{
"& .alert.row-stack": {
backgroundColor: theme.palette.warningSecondary.main,
borderColor: theme.palette.warningSecondary.lowContrast,
"& .MuiTypography-root": {
color: theme.palette.warningSecondary.contrastText,
},
"& .MuiBox-root > svg": {
color: theme.palette.warningSecondary.contrastText,
},
},
}}
>
{settingsData?.pagespeedKeySet === false && (
<Alert
variant="warning"
hasIcon={true}
body={renderWarningMessage(t)}
/>
)}
</Box>
</Box>
);
};
FallbackPageSpeedWarning.propTypes = {
settingsData: PropTypes.shape({
pagespeedKeySet: PropTypes.bool,
}),
};
export default FallbackPageSpeedWarning;
@@ -1,21 +0,0 @@
import { useTheme } from "@mui/material/styles";
import { Typography } from "@mui/material";
import PropTypes from "prop-types";
const FallbackTitle = ({ title }) => {
const theme = useTheme();
return (
<Typography
alignSelf="center"
component="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastText}
>
{title}
</Typography>
);
};
FallbackTitle.propTypes = {
title: PropTypes.string.isRequired,
};
export default FallbackTitle;
@@ -1,40 +0,0 @@
[class*="fallback__"] {
width: fit-content;
margin: auto;
}
[class*="fallback__"] h1.MuiTypography-root {
font-size: var(--env-var-font-size-large);
font-weight: 600;
}
[class*="fallback__"] button.MuiButtonBase-root,
[class*="fallback__"] .check {
width: max-content;
}
[class*="fallback__"] button.MuiButtonBase-root {
min-height: 34px;
}
[class*="fallback__"] .check span.MuiTypography-root,
[class*="fallback__"] button.MuiButtonBase-root {
font-size: var(--env-var-font-size-medium);
}
[class*="fallback__"] .background-pattern-svg {
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, -50%);
z-index: 0;
width: 100%;
max-width: 800px;
height: 100%;
max-height: 800px;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
}
@@ -1,75 +0,0 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Stack } from "@mui/material";
import "./index.css";
import FallbackTitle from "./FallbackTitle.jsx";
import FallbackCheckList from "./FallbackCheckList.jsx";
import FallbackActionButtons from "./FallBackActionButtons.jsx";
import FallbackBackground from "./FallbackBackground.jsx";
import FallbackContainer from "./FallbackContainer.jsx";
/**
* Fallback component to display a fallback UI with a title, a list of checks, and a navigation button.
*
* @param {Object} props - The component props.
* @param {string} props.title - The title to be displayed in the fallback UI.
* @param {string} props.type - The type of the fallback (e.g., "pageSpeed", "notifications").
* @param {Array<string>} props.checks - An array of strings representing the checks to display.
* @param {string} [props.link="/"] - The link to navigate to.
* @param {boolean} [props.showPageSpeedWarning=false] - Whether to show the PageSpeed API warning.
* @returns {JSX.Element} The rendered fallback UI.
*/
const Fallback = ({ title, checks, link = "/", isAdmin, type, children }) => {
const theme = useTheme();
return (
<Box
position="relative"
sx={{
height: "100%",
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<FallbackContainer type={type}>
<FallbackBackground />
<Stack
gap={theme.spacing(5)}
zIndex={1}
alignItems="center"
>
<FallbackTitle title={title} />
<FallbackCheckList
checks={checks}
title={title}
type={type}
/>
</Stack>
{isAdmin && (
<Stack
gap={theme.spacing(10)}
alignItems="center"
>
<FallbackActionButtons
title={title}
link={link}
type={type}
/>
{children}
</Stack>
)}
</FallbackContainer>
</Box>
);
};
Fallback.propTypes = {
title: PropTypes.string.isRequired,
checks: PropTypes.arrayOf(PropTypes.string).isRequired,
link: PropTypes.string,
isAdmin: PropTypes.bool,
showPageSpeedWarning: PropTypes.bool,
type: PropTypes.string.isRequired,
children: PropTypes.node,
};
export default Fallback;
@@ -1,23 +0,0 @@
import { Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
const NetworkError = () => {
const theme = useTheme();
const { t } = useTranslation();
return (
<>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{t("common.toasts.networkError")}
</Typography>
<Typography>{t("common.toasts.checkConnection")}</Typography>
</>
);
};
export default NetworkError;
@@ -1,85 +0,0 @@
import { useTheme } from "@emotion/react";
import { Box, Stack } from "@mui/material";
import OutputAnimation from "../../../assets/Animations/output.gif";
import DarkmodeOutput from "../../../assets/Animations/darkmodeOutput.gif";
import Background from "../../../assets/Images/background-grid.svg?react";
import { useSelector } from "react-redux";
/**
* Fallback component to display a fallback UI for network errors
*
* @returns {JSX.Element} The rendered fallback UI.
*/
const GenericFallback = ({ children }) => {
const theme = useTheme();
const mode = useSelector((state) => state.ui.mode);
return (
<Box
padding={theme.spacing(16)}
position="relative"
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.primary.main}
overflow="hidden"
sx={{
borderStyle: "dashed",
}}
>
<Stack
alignItems="center"
gap={theme.spacing(20)}
sx={{
width: "fit-content",
margin: "auto",
marginTop: "100px",
}}
>
<Box
component="img"
src={mode === "light" ? OutputAnimation : DarkmodeOutput}
alt="Loading animation"
sx={{
zIndex: 1,
border: "none",
borderRadius: theme.spacing(8),
width: "100%",
backgroundColor: "transparent",
}}
/>
<Box
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
position: "absolute",
top: "0",
left: "50%",
transform: "translate(-50%, -50%)",
width: "100%",
maxWidth: "800px",
height: "100%",
maxHeight: "800px",
backgroundPosition: "center",
backgroundSize: "cover",
backgroundRepeat: "no-repeat",
}}
>
<Background style={{ width: "100%" }} />
</Box>
<Stack
gap={theme.spacing(4)}
alignItems="center"
maxWidth={"300px"}
zIndex={1}
>
{children}
</Stack>
</Stack>
</Box>
);
};
export default GenericFallback;
@@ -1,49 +0,0 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { logger } from "../../../Utils/Logger.js";
import { networkService } from "../../../main.jsx";
const withAdminCheck = (WrappedComponent) => {
const WithAdminCheck = (props) => {
const navigate = useNavigate();
const [isChecking, setIsChecking] = useState(true);
const [superAdminExists, setSuperAdminExists] = useState(false);
useEffect(() => {
networkService
.doesSuperAdminExist()
.then((response) => {
if (response?.data?.data === true) {
navigate("/login");
} else {
setSuperAdminExists(false);
}
})
.catch((error) => {
logger.error(error);
})
.finally(() => {
setIsChecking(false);
});
}, [navigate]);
if (isChecking) {
return null;
}
return (
<WrappedComponent
{...props}
superAdminExists={superAdminExists}
/>
);
};
const wrappedComponentName =
WrappedComponent.displayName || WrappedComponent.name || "Component";
WithAdminCheck.displayName = `WithAdminCheck(${wrappedComponentName})`;
return WithAdminCheck;
};
export default withAdminCheck;
@@ -1,32 +0,0 @@
import { Typography } from "@mui/material";
import PropTypes from "prop-types";
/**
* A heading component that renders text with a specific heading level.
*
* @param {Object} props - The properties passed to the Heading component.
* @param {('h1'|'h2'|'h3')} props.component - The heading level for the component.
* @param {Object} props.style - Custom styles to apply to the heading.
* @param {string} props.children - The content to display inside the heading.
* @returns {JSX.Element} The Typography component with specified heading properties.
*/
function Heading({ component, style, children }) {
return (
<Typography
component={component}
variant="h2"
fontWeight={600}
style={style}
>
{children}
</Typography>
);
}
Heading.propTypes = {
component: PropTypes.oneOf(["h1", "h2", "h3"]).isRequired,
style: PropTypes.object,
children: PropTypes.string.isRequired,
};
export { Heading };
-73
View File
@@ -1,73 +0,0 @@
import { Stack, Typography } from "@mui/material";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import Dot from "../Dot/index.jsx";
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
/**
* Host component.
* This subcomponent receives a params object and displays the host details.
*
* @component
* @param {Object} params - An object containing the following properties:
* @param {string} params.url - The URL of the host.
* @param {string} params.title - The name of the host.
* @param {string} params.percentageColor - The color of the percentage text.
* @param {number} params.percentage - The percentage to display.
* @returns {React.ElementType} Returns a div element with the host details.
*/
const Host = ({ url, title, percentageColor, percentage, showURL, status }) => {
const theme = useTheme();
return (
<Stack>
<Stack
direction="row"
position="relative"
alignItems="center"
gap={theme.spacing(4)}
>
<Typography
component="span"
sx={{
fontWeight: 500,
}}
>
{title}
</Typography>
{percentageColor && percentage && (
<>
<Dot />
<Typography
component="span"
sx={{
fontWeight: 500,
color: percentageColor,
}}
>
{percentage}%
</Typography>
{status && (
<StatusLabel
status={status}
text={status}
customStyles={{ textTransform: "capitalize" }}
/>
)}
</>
)}
</Stack>
{showURL && <span style={{ opacity: 0.6 }}>{url}</span>}
</Stack>
);
};
Host.propTypes = {
title: PropTypes.string,
percentageColor: PropTypes.string,
percentage: PropTypes.string,
url: PropTypes.string,
showURL: PropTypes.bool,
status: PropTypes.string,
};
export default Host;
@@ -1,67 +0,0 @@
import PropTypes from "prop-types";
import { useTheme } from "@mui/material";
import { BaseLabel } from "../Label/index.jsx";
/**
* @component
* @param {Object} props
* @param {number} props.status - The http status for the label
* @param {Styles} props.customStyles - CSS Styles passed from parent component
* @returns {JSX.Element}
* @example
* // Render a http status label
* <HttpStatusLabel status={404} />
*/
const DEFAULT_CODE = 9999; // Default code for unknown status
const handleStatusCode = (status) => {
if (status) {
return status;
}
return DEFAULT_CODE;
};
const getRoundedStatusCode = (status) => {
return Math.floor(status / 100) * 100;
};
const HttpStatusLabel = ({ status, customStyles }) => {
const theme = useTheme();
const colors = {
400: {
color: theme.palette.warning.main,
borderColor: theme.palette.warning.lowContrast,
},
500: {
color: theme.palette.error.main,
borderColor: theme.palette.error.lowContrast,
},
default: {
color: theme.palette.primary.contrastText,
borderColor: theme.palette.primary.contrastText,
},
};
const statusCode = handleStatusCode(status);
const { borderColor, color } =
colors[getRoundedStatusCode(statusCode)] || colors.default;
return (
<BaseLabel
label={String(statusCode)}
styles={{
color: color,
borderColor: borderColor,
...customStyles,
}}
/>
);
};
HttpStatusLabel.propTypes = {
status: PropTypes.number,
customStyles: PropTypes.object,
};
export { HttpStatusLabel };
@@ -1,77 +0,0 @@
import { Box, styled } from "@mui/material";
import PropTypes from "prop-types";
/**
* IconBox - A styled box component for rendering icons with consistent sizing and styling
*
* @component
* @param {Object} [props] - Configuration options for the IconBox
* @param {number} [props.height=34] - Height of the icon box
* @param {number} [props.width=34] - Width of the icon box
* @param {number} [props.minWidth=34] - Minimum width of the icon box
* @param {number} [props.borderRadius=4] - Border radius of the icon box
* @param {number} [props.svgWidth=20] - Width of the SVG icon
* @param {number} [props.svgHeight=20] - Height of the SVG icon
*
* @example
* // Basic usage
* <IconBox>
* <SomeIcon />
* </IconBox>
*
* @example
* // Customized usage
* <IconBox
* height={40}
* width={40}
* svgWidth={24}
* svgHeight={24}
* >
* <CustomIcon />
* </IconBox>
*
* @returns {React.ReactElement} A styled box containing an icon
*/
const IconBox = styled(Box)(
({
theme,
height = 34,
width = 34,
minWidth = 34,
borderRadius = 4,
svgWidth = 20,
svgHeight = 20,
}) => ({
height: height,
minWidth: minWidth,
width: width,
position: "relative",
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
borderRadius: borderRadius,
backgroundColor: theme.palette.tertiary.main,
"& svg": {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: svgWidth,
height: svgHeight,
"& path": {
stroke: theme.palette.primary.contrastTextTertiary,
},
},
})
);
IconBox.propTypes = {
height: PropTypes.number,
width: PropTypes.number,
minWidth: PropTypes.number,
borderRadius: PropTypes.number,
svgWidth: PropTypes.number,
svgHeight: PropTypes.number,
};
export default IconBox;
-72
View File
@@ -1,72 +0,0 @@
import { Box } from "@mui/material";
import PropTypes from "prop-types";
const isValidBase64Image = (data) => {
return /^[A-Za-z0-9+/=]+$/.test(data);
};
const Image = ({
shouldRender = true,
src,
alt,
width = "auto",
height = "auto",
minWidth = "auto",
minHeight = "auto",
maxWidth = "auto",
maxHeight = "auto",
base64,
placeholder,
sx,
}) => {
if (shouldRender === false) {
return null;
}
if (typeof src !== "undefined" && typeof base64 !== "undefined") {
console.warn("base64 takes precedence over src and overwrites it");
}
if (typeof base64 !== "undefined" && isValidBase64Image(base64)) {
src = `data:image/png;base64,${base64}`;
}
if (
typeof src === "undefined" &&
typeof base64 === "undefined" &&
typeof placeholder !== "undefined"
) {
src = placeholder;
}
return (
<Box
component="img"
src={src}
alt={alt}
minWidth={minWidth}
minHeight={minHeight}
maxWidth={maxWidth}
maxHeight={maxHeight}
width={width}
height={height}
sx={{ ...sx }}
/>
);
};
Image.propTypes = {
shouldRender: PropTypes.bool,
src: PropTypes.string,
alt: PropTypes.string.isRequired,
width: PropTypes.string,
height: PropTypes.string,
minWidth: PropTypes.string,
minHeight: PropTypes.string,
maxWidth: PropTypes.string,
maxHeight: PropTypes.string,
base64: PropTypes.string,
sx: PropTypes.object,
};
export default Image;
@@ -1,42 +0,0 @@
import { RowContainer } from "../StandardContainer/index.jsx";
import { Stack, Typography } from "@mui/material";
import Image from "../Image/index.jsx";
import { useTheme } from "@mui/material/styles";
const InfoBox = ({
img,
icon: Icon,
alt,
heading,
headingLevel = 2,
subHeading,
subHeadingLevel = "",
sx,
}) => {
const theme = useTheme();
return (
<RowContainer sx={{ ...sx }}>
{img && (
<Image
src={img}
height={"30px"}
width={"30px"}
alt={alt}
sx={{ marginRight: theme.spacing(8) }}
/>
)}
{Icon && (
<Icon sx={{ width: "30px", height: "30px", marginRight: theme.spacing(8) }} />
)}
<Stack>
<Typography variant={`h${headingLevel}`}>{heading}</Typography>
<Typography variant={subHeadingLevel ? `h${subHeadingLevel}` : "body1"}>
{subHeading}
</Typography>
</Stack>
</RowContainer>
);
};
export default InfoBox;
@@ -64,7 +64,6 @@ const Select = ({
const theme = useTheme();
const getItemValue = (item) => item?._id ?? item?.id;
const itemStyles = {
fontSize: "var(--env-var-font-size-medium)",
color: theme.palette.primary.contrastTextTertiary,
borderRadius: theme.shape.borderRadius,
margin: theme.spacing(2),
@@ -18,7 +18,6 @@ export const HttpAdornment = ({ https }) => {
>
<Typography
component="h5"
paddingRight={"var(--env-var-spacing-1-minus)"}
color={theme.palette.primary.contrastTextSecondary}
sx={{ lineHeight: 1, opacity: 0.8 }}
>
-6
View File
@@ -1,6 +0,0 @@
.label {
display: inline-flex;
justify-content: center;
align-items: center;
line-height: normal;
}
-172
View File
@@ -1,172 +0,0 @@
import PropTypes from "prop-types";
import { Box } from "@mui/material";
import { useTheme } from "@mui/material";
import "./index.css";
/**
* @typedef {Object} Styles
* @param {string} [color] - The text color
* @param {string} [backgroundColor] - The background color
* @param {string} [borderColor] - The border color
*/
/**
* @component
* @param {Object} props
* @param {string} props.label - The label of the label
* @param {Styles} props.styles - CSS Styles passed from parent component
* @param {React.ReactNode} children - Children passed from parent component
* @returns {JSX.Element}
*/
const BaseLabel = ({ label, styles, children }) => {
const theme = useTheme();
// Grab the default borderRadius from the theme to match button style
const { borderRadius } = theme.shape;
// Calculate padding for the label to mimic button. Appears to scale correctly, not 100% sure though.
const padding = theme.spacing(3, 5);
return (
<Box
className="label"
sx={{
borderRadius: borderRadius,
border: `1px solid ${theme.palette.primary.lowContrast}`,
color: theme.palette.primary.contrastText,
padding: padding,
...styles,
}}
>
{children}
{label}
</Box>
);
};
BaseLabel.propTypes = {
label: PropTypes.string.isRequired,
styles: PropTypes.shape({
color: PropTypes.string,
backgroundColor: PropTypes.string,
}),
children: PropTypes.node,
};
// Produces a lighter color based on a hex color and a percent
// lightenColor("#067647", 20) will produce a color 20% lighter than #067647
const lightenColor = (color, percent) => {
let r = parseInt(color.substring(1, 3), 16);
let g = parseInt(color.substring(3, 5), 16);
let b = parseInt(color.substring(5, 7), 16);
const amt = Math.round((255 * percent) / 100);
r = r + amt <= 255 ? r + amt : 255;
g = g + amt <= 255 ? g + amt : 255;
b = b + amt <= 255 ? b + amt : 255;
r = r.toString(16).padStart(2, "0");
g = g.toString(16).padStart(2, "0");
b = b.toString(16).padStart(2, "0");
return `#${r}${g}${b}`;
};
/**
* @component
* @param {Object} props
* @param {string} props.label - The label of the label
* @param {string} props.color - The color of the label, specified in #RRGGBB format
* @returns {JSX.Element}
* @example
* // Render a red label
* <ColoredLabel label="Label" color="#FF0000" />
*/
const ColoredLabel = ({ label, color }) => {
const theme = useTheme();
// If an invalid color is passed, default to the labelGray color
if (typeof color !== "string" || !/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(color)) {
color = theme.palette.primary.lowContrast;
}
// Calculate lighter shades for border and bg
const borderColor = lightenColor(color, 20);
const bgColor = lightenColor(color, 75);
return (
<BaseLabel
label={label}
styles={{
color: color,
borderColor: borderColor,
backgroundColor: bgColor,
}}
></BaseLabel>
);
};
ColoredLabel.propTypes = {
label: PropTypes.string.isRequired,
color: PropTypes.string.isRequired,
};
/**
* @component
* @param {Object} props
* @param {'up' | 'down' | 'paused' | 'pending' | 'cannot resolve' | 'published' | 'unpublished'} props.status - The status for the label
* @param {string} props.text - The text of the label
* @returns {JSX.Element}
* @example
* // Render an active label
* <StatusLabel status="up" text="Active" />
*/
const statusToTheme = {
up: "success",
down: "error",
paused: "warning",
pending: "warning",
"cannot resolve": "error",
published: "success",
unpublished: "error",
};
const StatusLabel = ({ status, text, customStyles }) => {
const theme = useTheme();
const themeColor = statusToTheme[status];
return (
<BaseLabel
label={text}
styles={{
color: theme.palette[themeColor].main,
borderColor: theme.palette[themeColor].lowContrast,
...customStyles,
}}
>
<Box
bgcolor={theme.palette[themeColor].lowContrast}
borderRadius="0%"
marginRight="1px"
/>
</BaseLabel>
);
};
StatusLabel.propTypes = {
status: PropTypes.oneOf([
"up",
"down",
"paused",
"pending",
"cannot resolve",
"published",
"unpublished",
]),
text: PropTypes.string,
customStyles: PropTypes.object,
};
export { BaseLabel, ColoredLabel, StatusLabel };
@@ -1,31 +0,0 @@
import Box from "@mui/material/Box";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import BackgroundSVG from "../../../../assets/Images/background.svg";
import { useSelector } from "react-redux";
const AppLayout = ({ children }) => {
const theme = useTheme();
const ui = useSelector((state) => state.ui);
return (
<Box
sx={{
minHeight: "100vh",
backgroundColor: theme.palette.primaryBackground.main,
backgroundImage: ui?.mode === "dark" ? `url("${BackgroundSVG}")` : "none",
backgroundSize: "100% 100%",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
color: theme.palette.primary.contrastText,
}}
>
{children}
</Box>
);
};
AppLayout.propTypes = {
children: PropTypes.node,
};
export default AppLayout;
@@ -1,29 +0,0 @@
.home-layout {
position: relative;
min-height: 100vh;
margin: 0 auto;
padding: 0;
overflow-x: hidden;
width: 100%;
}
/* TODO go for this approach for responsiveness. The aside needs to be taken care of */
/* @media (max-width: 1000px) {
.home-layout {
flex-direction: column !important;
}
} */
.home-layout > .home-content-wrapper {
min-height: calc(100vh - var(--env-var-spacing-2) * 2);
flex: 1;
}
.home-content-wrapper {
padding: var(--env-var-spacing-2);
max-width: 1400px;
margin: 0 auto;
flex: 1;
min-width: 0;
overflow-x: hidden;
}
@@ -1,32 +0,0 @@
import Sidebar from "../../Sidebar/index.jsx";
import { Outlet } from "react-router";
import { Box, Stack } from "@mui/material";
import { useSidebar } from "@/Hooks/useSidebar.js";
import "./index.css";
const HomeLayout = () => {
const { width, transition } = useSidebar();
return (
<Stack
className="home-layout"
flexDirection="row"
>
<Sidebar />
{/* Spacer for fixed sidebar */}
<Box
sx={{
width,
flexShrink: 0,
transition,
}}
/>
<Stack className="home-content-wrapper">
<Outlet />
</Stack>
</Stack>
);
};
export default HomeLayout;
@@ -1,44 +0,0 @@
import { Stack, Button } from "@mui/material";
import { useNavigate } from "react-router-dom";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
const CreateMonitorHeader = ({ isAdmin, label, isLoading = true, path, bulkPath }) => {
const navigate = useNavigate();
const { t } = useTranslation();
const theme = useTheme();
// Use the provided label or fall back to the translated default
if (!isAdmin) return null;
return (
<Stack
direction="row"
justifyContent="end"
alignItems="center"
gap={theme.spacing(6)}
>
<Button
loading={isLoading}
variant="contained"
color="accent"
onClick={() => navigate(path)}
>
{label || t("createNew")}
</Button>
{/* {bulkPath && <MonitorActions isLoading={isLoading} />} */}
</Stack>
);
};
export default CreateMonitorHeader;
CreateMonitorHeader.propTypes = {
isAdmin: PropTypes.bool.isRequired,
isLoading: PropTypes.bool,
path: PropTypes.string.isRequired,
label: PropTypes.string,
bulkPath: PropTypes.string,
};
@@ -1,19 +0,0 @@
import { Stack, Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Stack
direction="row"
justifyContent="end"
alignItems="center"
>
<Skeleton
variant="rectangular"
width={100}
height={36}
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -1,85 +0,0 @@
import { Stack, Typography, Button, ButtonGroup } from "@mui/material";
import { useTheme } from "@emotion/react";
import SkeletonLayout from "./skeleton.jsx";
import PropTypes from "prop-types";
const MonitorTimeFrameHeader = ({
isLoading = false,
hasDateRange = true,
dateRange,
setDateRange,
}) => {
const theme = useTheme();
if (isLoading) {
return <SkeletonLayout />;
}
let timeFramePicker = null;
if (hasDateRange) {
timeFramePicker = (
<ButtonGroup sx={{ height: 32 }}>
<Button
variant="group"
filled={(dateRange === "recent").toString()}
onClick={() => setDateRange("recent")}
>
Recent
</Button>
<Button
variant="group"
filled={(dateRange === "day").toString()}
onClick={() => setDateRange("day")}
>
Day
</Button>
<Button
variant="group"
filled={(dateRange === "week").toString()}
onClick={() => setDateRange("week")}
>
Week
</Button>
<Button
variant="group"
filled={(dateRange === "month").toString()}
onClick={() => setDateRange("month")}
>
Month
</Button>
</ButtonGroup>
);
}
return (
<Stack
direction="row"
justifyContent="flex-end"
alignItems="center"
gap={theme.spacing(4)}
>
<Typography variant="body2">
Showing statistics for past{" "}
{dateRange === "recent"
? "2 hours"
: dateRange === "day"
? "24 hours"
: dateRange === "week"
? "7 days"
: "30 days"}
.
</Typography>
{timeFramePicker}
</Stack>
);
};
MonitorTimeFrameHeader.propTypes = {
isLoading: PropTypes.bool,
hasDateRange: PropTypes.bool,
dateRange: PropTypes.string,
setDateRange: PropTypes.func,
};
export default MonitorTimeFrameHeader;
@@ -1,23 +0,0 @@
import { Stack, Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Stack
direction="row"
justifyContent="space-between"
>
<Skeleton
variant="rounded"
width="20%"
height={34}
/>
<Skeleton
variant="rounded"
width="20%"
height={34}
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -1,66 +0,0 @@
import GenericFallback from "../GenericFallback/index.jsx";
import Fallback from "../Fallback/index.jsx";
import { Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
import PropTypes from "prop-types";
const PageStateWrapper = ({
networkError,
isLoading,
items,
type,
fallbackLink,
fallbackChildren,
children,
}) => {
const theme = useTheme();
const { t } = useTranslation();
const isAdmin = useIsAdmin();
const isEmpty = items === null || (Array.isArray(items) && items.length === 0);
const hasInitialized = items !== undefined && !(isLoading && isEmpty);
const showEmpty = !isLoading && isEmpty;
if (networkError) {
return (
<GenericFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{t("common.toasts.networkError")}
</Typography>
<Typography>{t("common.toasts.checkConnection")}</Typography>
</GenericFallback>
);
}
if (!hasInitialized) {
return null;
}
if (showEmpty) {
return (
<Fallback
type={type}
title={t(`${type}.fallback.title`)}
checks={t(`${type}.fallback.checks`, { returnObjects: true })}
link={fallbackLink}
isAdmin={isAdmin}
>
{fallbackChildren}
</Fallback>
);
}
return <>{children}</>;
};
PageStateWrapper.propTypes = {
networkError: PropTypes.any,
isLoading: PropTypes.bool,
items: PropTypes.oneOfType([PropTypes.array, PropTypes.oneOf([null])]),
type: PropTypes.string,
fallbackLink: PropTypes.string,
fallbackChildren: PropTypes.node,
children: PropTypes.node,
};
export default PageStateWrapper;
@@ -1,10 +0,0 @@
.progress-bar-container h2.MuiTypography-root,
.progress-bar-container p.MuiTypography-root {
font-size: var(--env-var-font-size-medium);
}
.progress-bar-container p.MuiTypography-root:has(span) {
font-size: 12px;
}
.progress-bar-container p.MuiTypography-root span {
padding-left: 2px;
}
@@ -1,180 +0,0 @@
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
import { Box, IconButton, LinearProgress, Stack, Typography } from "@mui/material";
import Icon from "../Icon";
import "./index.css";
/**
* @param {Object} props - The component props.
* @param {JSX.Element} props.icon - The icon element to display (optional).
* @param {string} props.label - The label text for the progress item.
* @param {string} props.size - The size information for the progress item.
* @param {number} props.progress - The current progress value (0-100).
* @param {function} props.onClick - The function to handle click events on the remove button.
* @param {string} props.error - Error message to display if there's an error (optional).
* @returns {JSX.Element} The rendered component.
*/
const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) => {
const theme = useTheme();
return (
<Box
className="progress-bar-container"
mt={theme.spacing(10)}
p={theme.spacing(8)}
sx={{
minWidth: "200px",
height: "fit-content",
borderRadius: theme.shape.borderRadius,
border: 1,
borderColor: theme.palette.primary.lowContrast,
backgroundColor: theme.palette.primary.lowContrast,
"&:has(.input-error)": {
borderColor: theme.palette.error.main,
backgroundColor: theme.palette.error.lowContrast,
py: theme.spacing(4),
px: theme.spacing(8),
"& > .MuiStack-root > svg": {
fill: theme.palette.error.contrastText,
width: "20px",
height: "20px",
},
},
}}
>
<Stack
direction="row"
mb={error ? 0 : theme.spacing(5)}
gap={theme.spacing(5)}
alignItems={error ? "center" : "flex-start"}
>
{error ? (
<Icon
name="AlertTriangle"
size={20}
/>
) : icon ? (
<Box
sx={{
position: "relative",
height: 30,
minWidth: 30,
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: 2,
backgroundColor: theme.palette.primary.main,
"& svg": {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 23,
height: 23,
"& path": {
fill: theme.palette.primary.contrastTextTertiary,
},
},
}}
>
{icon}
</Box>
) : (
""
)}
{error ? (
<Typography
component="p"
className="input-error"
color={theme.palette.error.contrastText}
>
{error}
</Typography>
) : (
<Box color={theme.palette.primary.contrastTextTertiary}>
<Typography
component="h2"
mb={theme.spacing(1.5)}
sx={{ wordBreak: "break-all" }}
>
{error ? error : label}
</Typography>
<Typography
component="p"
sx={{ opacity: 0.6 }}
>
{!error && size}
</Typography>
</Box>
)}
<IconButton
onClick={onClick}
sx={
!error
? {
alignSelf: "flex-start",
ml: "auto",
mr: theme.spacing(-2.5),
mt: theme.spacing(-2.5),
padding: theme.spacing(2.5),
"&:focus": {
outline: "none",
},
}
: {
ml: "auto",
"&:focus": {
outline: "none",
},
}
}
>
<Icon
name="X"
size={20}
/>
</IconButton>
</Stack>
{!error ? (
<Stack
direction="row"
alignItems="center"
>
<Box sx={{ width: "100%", mr: theme.spacing(5) }}>
<LinearProgress
variant="determinate"
value={progress}
sx={{
width: "100%",
height: "10px",
borderRadius: theme.shape.borderRadius,
maxWidth: "500px",
backgroundColor: theme.palette.primary.lowContrast,
}}
/>
</Box>
<Typography
component="p"
sx={{ minWidth: "max-content", opacity: 0.6 }}
>
{progress}
<span>%</span>
</Typography>
</Stack>
) : (
""
)}
</Box>
);
};
ProgressUpload.propTypes = {
icon: PropTypes.element, // JSX element for the icon (optional)
label: PropTypes.string, // Label text for the progress item
size: PropTypes.string.isRequired, // Size information for the progress item
progress: PropTypes.number.isRequired, // Current progress value (0-100)
onClick: PropTypes.func.isRequired, // Function to handle click events on the remove button
error: PropTypes.string, // Error message to display if there's an error (optional)
};
export default ProgressUpload;
Whitespace-only changes.
@@ -1,74 +0,0 @@
import React from "react";
import { Step, Stepper, StepLabel, Typography } from "@mui/material";
import Icon from "../Icon";
import PropTypes from "prop-types";
const CustomStepIcon = (props) => {
const { completed, active } = props;
return completed ? (
<Icon
name="CheckCircle2"
size={24}
color="accent.main"
/>
) : (
<Icon
name="Circle"
size={24}
color={active ? "accent.main" : "text.disabled"}
/>
);
};
CustomStepIcon.propTypes = {
completed: PropTypes.bool.isRequired,
active: PropTypes.bool.isRequired,
};
/**
* @component
* @param {Object} props
* @param {Array} props.steps
*/
const ProgressStepper = ({ steps }) => {
const [activeStep, setActiveStep] = React.useState(1);
return (
<Stepper
activeStep={activeStep}
alternativeLabel
>
{steps.map((step, index) => {
const color = activeStep === index ? "primary" : "inherit";
return (
<Step
key={step.label}
onClick={() => setActiveStep(index)}
>
<StepLabel StepIconComponent={CustomStepIcon}>
<Typography
variant="body1"
color={color}
sx={{ fontWeight: "bold" }}
>
{step.label}
</Typography>
</StepLabel>
<Typography
variant="body1"
color={color}
>
{step.content}
</Typography>
</Step>
);
})}
</Stepper>
);
};
ProgressStepper.propTypes = {
steps: PropTypes.array.isRequired,
};
export default ProgressStepper;
@@ -1,32 +0,0 @@
import { Navigate } from "react-router-dom";
import { useSelector } from "react-redux";
import PropTypes from "prop-types";
/**
* ProtectedRoute is a wrapper component that ensures only authenticated users
* can access the wrapped content. It checks authentication status (e.g., from Redux or Context).
* If the user is authenticated, it renders the children; otherwise, it redirects to the login page.
*
* @param {Object} props - The props passed to the ProtectedRoute component.
* @param {React.ReactNode} props.children - The children to render if the user is authenticated.
* @returns {React.ReactElement} The children wrapped in a protected route or a redirect to the login page.
*/
const ProtectedRoute = ({ children }) => {
const authState = useSelector((state) => state.auth);
return authState.authToken ? (
children
) : (
<Navigate
to="/login"
replace
/>
);
};
ProtectedRoute.propTypes = {
children: PropTypes.element.isRequired,
};
export default ProtectedRoute;
@@ -1,35 +0,0 @@
import { Navigate } from "react-router-dom";
import { useSelector } from "react-redux";
import PropTypes from "prop-types";
/**
* ProtectedRoute is a wrapper component that ensures only authenticated users
* can access the wrapped content. It checks authentication status (e.g., from Redux or Context).
* If the user is authenticated, it renders the children; otherwise, it redirects to the login page.
*
* @param {Object} props - The props passed to the ProtectedRoute component.
* @param {React.ReactNode} props.children - The children to render if the user is authenticated.
* @returns {React.ReactElement} The children wrapped in a protected route or a redirect to the login page.
*/
const RoleProtectedRoute = ({ roles, children }) => {
const authState = useSelector((state) => state.auth);
const userRoles = authState?.user?.role || [];
const canAccess = userRoles.some((role) => roles.includes(role));
return canAccess ? (
children
) : (
<Navigate
to="/uptime"
replace
/>
);
};
RoleProtectedRoute.propTypes = {
children: PropTypes.element.isRequired,
roles: PropTypes.array,
};
export default RoleProtectedRoute;
@@ -1,73 +0,0 @@
import html2canvas from "html2canvas";
import Icon from "../Icon";
import { Button } from "@mui/material";
import { useTheme } from "@emotion/react";
const ShareComponent = ({ elementToCapture, fileName = "screenshot" }) => {
const theme = useTheme();
const captureAndShare = async () => {
try {
// Temporarily apply styles directly to the element
const originalBackground = elementToCapture.current.style.background;
const originalPadding = elementToCapture.current.style.padding;
elementToCapture.current.style.background = `radial-gradient(circle, ${theme.palette.gradient.color1}, ${theme.palette.gradient.color2}, ${theme.palette.gradient.color3}, ${theme.palette.gradient.color4}, ${theme.palette.gradient.color5})`;
elementToCapture.current.style.padding = `${theme.spacing(20)}`;
// Capture the element directly
const canvas = await html2canvas(elementToCapture.current, {
useCORS: true,
scale: 2,
allowTaint: true,
backgroundColor: null,
});
// Restore original styles
elementToCapture.current.style.background = originalBackground;
elementToCapture.current.style.padding = originalPadding;
const imageBlob = await new Promise((resolve) =>
canvas.toBlob(resolve, "image/png")
);
const file = new File([imageBlob], `${fileName}.png`, {
type: "image/png",
});
if (navigator.share) {
await navigator.share({
files: [file],
title: "Screenshot",
text: "Check out this screenshot!",
});
} else {
const url = URL.createObjectURL(imageBlob);
const a = document.createElement("a");
a.href = url;
a.download = `${fileName}.png`;
a.click();
URL.revokeObjectURL(url);
}
} catch (error) {
console.error(error);
}
};
return (
<Button
variant="outlined"
startIcon={
<Icon
name="Share2"
size={18}
color="success.main"
/>
}
color="success"
onClick={captureAndShare}
>
Share
</Button>
);
};
export default ShareComponent;
@@ -1,276 +0,0 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Tooltip from "@mui/material/Tooltip";
import IconButton from "@mui/material/IconButton";
import Avatar from "../../Avatar/index.jsx";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import Divider from "@mui/material/Divider";
import Icon from "../../Icon";
import { useTheme } from "@emotion/react";
import { useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { useState } from "react";
import { useNavigate } from "react-router";
import { clearAuthState } from "../../../../Features/Auth/authSlice.js";
import { useDispatch } from "react-redux";
import PropTypes from "prop-types";
const getFilteredAccountMenuItems = (user, items) => {
if (!user) return [];
let filtered = [...items];
if (user.role?.includes("demo")) {
filtered = filtered.filter((item) => item.name !== "Password");
}
if (!user.role?.includes("superadmin")) {
filtered = filtered.filter((item) => item.name !== "Team");
}
return filtered;
};
const getRoleDisplayText = (user, t) => {
if (!user?.role) return "";
if (user.role.includes("superadmin")) return t("roles.superAdmin");
if (user.role.includes("admin")) return t("roles.admin");
if (user.role.includes("user")) return t("roles.teamMember");
if (user.role.includes("demo")) return t("roles.demoUser");
return user.role;
};
const AuthFooter = ({ collapsed, accountMenuItems }) => {
const { t } = useTranslation();
const theme = useTheme();
const authState = useSelector((state) => state.auth);
const navigate = useNavigate();
const dispatch = useDispatch();
const [anchorEl, setAnchorEl] = useState(null);
const openPopup = (event) => {
setAnchorEl(event.currentTarget);
};
const closePopup = () => {
setAnchorEl(null);
};
const logout = async () => {
dispatch(clearAuthState());
navigate("/login");
};
const renderAccountMenuItems = (user, items) => {
const filteredItems = getFilteredAccountMenuItems(user, items);
return filteredItems.map((item) => (
<MenuItem
key={item.name}
onClick={() => {
closePopup();
navigate(item.path);
}}
sx={{
gap: theme.spacing(2),
borderRadius: theme.shape.borderRadius,
pl: theme.spacing(4),
color: theme.palette.primary.contrastTextTertiary,
"& svg": {
stroke: theme.palette.primary.contrastTextTertiary,
},
}}
>
{item.icon}
{item.name}
</MenuItem>
));
};
return (
<Stack
direction="row"
height="var(--env-var-side-bar-auth-footer-height)"
alignItems="center"
py={theme.spacing(4)}
px={theme.spacing(8)}
gap={theme.spacing(2)}
borderRadius={theme.shape.borderRadius}
boxSizing={"border-box"}
>
<Avatar
small={true}
onClick={(e) => collapsed && openPopup(e)}
sx={{
cursor: collapsed ? "pointer" : "default",
}}
/>
<Stack
direction={"row"}
alignItems={"center"}
gap={theme.spacing(2)}
minWidth={0}
maxWidth={collapsed ? 0 : "100%"}
sx={{
opacity: collapsed ? 0 : 1,
transition: "opacity 300ms ease, max-width 300ms ease",
transitionDelay: collapsed ? "0ms" : "300ms",
}}
>
<Stack
ml={theme.spacing(2)}
sx={{
maxWidth: "50%",
overflow: "hidden",
}}
>
<Typography
color={theme.palette.primary.contrastText}
fontWeight={500}
lineHeight={1}
fontSize={"var(--env-var-font-size-medium)"}
sx={{
display: "block",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{authState.user?.firstName} {authState.user?.lastName}
</Typography>
<Typography
color={theme.palette.primary.contrastText}
fontSize={"var(--env-var-font-size-small)"}
textOverflow="ellipsis"
overflow="hidden"
whiteSpace="nowrap"
sx={{ textTransform: "capitalize", opacity: 0.8 }}
>
{getRoleDisplayText(authState.user, t)}
</Typography>
</Stack>
<Tooltip
title={t("navControls")}
disableInteractive
>
<IconButton
sx={{
ml: "50px",
"&:focus": { outline: "none" },
alignSelf: "center",
}}
onClick={(event) => openPopup(event)}
>
<Icon
name="MoreVertical"
size={22}
color="primary.contrastTextTertiary"
/>
</IconButton>
</Tooltip>
</Stack>
<Menu
className="sidebar-popup"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={closePopup}
disableScrollLock
anchorOrigin={{
vertical: "top",
horizontal: "right",
}}
slotProps={{
paper: {
sx: {
marginTop: theme.spacing(-4),
marginLeft: collapsed ? theme.spacing(2) : 0,
},
},
}}
MenuListProps={{
sx: {
p: 2,
"& li": { m: 0 },
"& li:has(.MuiBox-root):hover": {
backgroundColor: "transparent",
},
},
}}
sx={{
ml: theme.spacing(4),
}}
>
{collapsed && (
<MenuItem sx={{ cursor: "default", minWidth: "50%" }}>
<Box
mb={theme.spacing(2)}
sx={{
minWidth: "50%",
maxWidth: "max-content",
overflow: "visible",
whiteSpace: "nowrap",
}}
>
<Typography
component="span"
fontWeight={500}
fontSize={13}
sx={{
display: "block",
whiteSpace: "nowrap",
overflow: "visible",
// wordBreak: "break-word",
textOverflow: "clip",
}}
>
{authState.user?.firstName} {authState.user?.lastName}
</Typography>
<Typography
sx={{
textTransform: "capitalize",
fontSize: 12,
whiteSpace: "nowrap",
overflow: "visible",
// wordBreak: "break-word",
}}
>
{authState.user?.role}
</Typography>
</Box>
</MenuItem>
)}
{/* TODO Do we need two dividers? */}
{collapsed && <Divider />}
{/* <Divider /> */}
{renderAccountMenuItems(authState.user, accountMenuItems)}
<MenuItem
onClick={logout}
sx={{
gap: theme.spacing(4),
borderRadius: theme.shape.borderRadius,
pl: theme.spacing(4),
}}
>
<Icon
name="LogOut"
size={20}
color="primary.contrastTextTertiary"
/>
{t("menu.logOut", "Log out")}
</MenuItem>
</Menu>
</Stack>
);
};
AuthFooter.propTypes = {
collapsed: PropTypes.bool,
accountMenuItems: PropTypes.array,
};
export default AuthFooter;
@@ -1,55 +0,0 @@
import IconButton from "@mui/material/IconButton";
import ArrowRight from "../../ArrowRight/index.jsx";
import ArrowLeft from "../../ArrowLeft/index.jsx";
import { useTheme } from "@mui/material/styles";
import { useDispatch } from "react-redux";
import { toggleSidebar } from "../../../../Features/UI/uiSlice.js";
import PropTypes from "prop-types";
const CollapseButton = ({ collapsed }) => {
const theme = useTheme();
const dispatch = useDispatch();
const arrowIcon = collapsed ? (
<ArrowRight
height={theme.spacing(8)}
width={theme.spacing(8)}
color={theme.palette.primary.contrastTextSecondary}
/>
) : (
<ArrowLeft
height={theme.spacing(8)}
width={theme.spacing(8)}
color={theme.palette.primary.contrastTextSecondary}
/>
);
return (
<IconButton
sx={{
position: "absolute",
/* TODO 60 is a magic number. if logo chnges size this might break */
top: 60,
right: 0,
transform: `translate(50%, 0)`,
backgroundColor: theme.palette.tertiary.main,
border: `1px solid ${theme.palette.primary.lowContrast}`,
p: theme.spacing(2.5),
"&:focus": { outline: "none" },
"&:hover": {
backgroundColor: theme.palette.primary.lowContrast,
borderColor: theme.palette.primary.lowContrast,
},
}}
onClick={() => {
dispatch(toggleSidebar());
}}
>
{arrowIcon}
</IconButton>
);
};
CollapseButton.propTypes = {
collapsed: PropTypes.bool.isRequired,
};
export default CollapseButton;
-152
View File
@@ -1,152 +0,0 @@
import Stack from "@mui/material/Stack";
import List from "@mui/material/List";
import Logo from "./components/logo.jsx";
import CollapseButton from "./components/collapseButton.jsx";
import Divider from "@mui/material/Divider";
import NavItem from "./components/navItem.jsx";
import AuthFooter from "./components/authFooter.jsx";
import StarPrompt from "../StarPrompt/index.jsx";
import Icon from "../Icon";
// Utils
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router";
import { useSidebar } from "@/Hooks/useSidebar.js";
const URL_MAP = {
support: "https://discord.com/invite/NAb6H3UTjK",
discussions: "https://github.com/bluewave-labs/checkmate/discussions",
docs: "https://bluewavelabs.gitbook.io/checkmate",
changelog: "https://github.com/bluewave-labs/checkmate/releases",
};
const getMenu = (t) => [
{ name: t("menu.uptime"), path: "uptime", icon: <Icon name="Globe" /> },
{ name: t("menu.pagespeed"), path: "pagespeed", icon: <Icon name="Gauge" /> },
{ name: t("menu.infrastructure"), path: "infrastructure", icon: <Icon name="Link" /> },
{
name: t("menu.notifications"),
path: "notifications",
icon: <Icon name="Bell" />,
},
{ name: t("menu.checks"), path: "checks", icon: <Icon name="FileText" /> },
{ name: t("menu.incidents"), path: "incidents", icon: <Icon name="AlertTriangle" /> },
{ name: t("menu.statusPages"), path: "status", icon: <Icon name="Wifi" /> },
{ name: t("menu.maintenance"), path: "maintenance", icon: <Icon name="Wrench" /> },
{ name: t("menu.logs"), path: "logs", icon: <Icon name="Database" /> },
{
name: t("menu.settings"),
icon: <Icon name="Settings" />,
path: "settings",
},
];
const getOtherMenuItems = (t) => [
{ name: t("menu.support"), path: "support", icon: <Icon name="HelpCircle" /> },
{
name: t("menu.discussions"),
path: "discussions",
icon: <Icon name="MessageCircle" />,
},
{ name: t("menu.docs"), path: "docs", icon: <Icon name="FileText" /> },
{ name: t("menu.changelog"), path: "changelog", icon: <Icon name="Code" /> },
];
const getAccountMenuItems = (t) => [
{ name: t("menu.profile"), path: "account/profile", icon: <Icon name="User" /> },
{ name: t("menu.password"), path: "account/password", icon: <Icon name="Lock" /> },
{ name: t("menu.team"), path: "account/team", icon: <Icon name="Users" /> },
];
const Sidebar = () => {
const theme = useTheme();
const { t } = useTranslation();
const navigate = useNavigate();
const { collapsed, width, transition } = useSidebar();
const menu = getMenu(t);
const otherMenuItems = getOtherMenuItems(t);
const accountMenuItems = getAccountMenuItems(t);
return (
<Stack
height="100vh"
width={width}
component="aside"
position="fixed"
top={0}
left={0}
paddingTop={theme.spacing(6)}
paddingBottom={theme.spacing(6)}
gap={theme.spacing(6)}
sx={{
transition,
backgroundColor: theme.palette.background.main,
borderRight: `1px solid ${theme.palette.primary.lowContrast}`,
zIndex: 1000,
}}
>
<CollapseButton collapsed={collapsed} />
<Logo collapsed={collapsed} />
<List
component="nav"
aria-labelledby="nested-menu-subheader"
disablePadding
sx={{
px: theme.spacing(6),
height: "100%",
}}
>
{menu.map((item) => {
const selected = location.pathname.startsWith(`/${item.path}`);
return (
<NavItem
key={item.path}
item={item}
collapsed={collapsed}
selected={selected}
onClick={() => navigate(`/${item.path}`)}
/>
);
})}
</List>
{!collapsed && <StarPrompt />}
<List
component="nav"
disablePadding
sx={{ px: theme.spacing(6) }}
>
{otherMenuItems.map((item) => {
const selected = location.pathname.startsWith(`/${item.path}`);
return (
<NavItem
key={item.path}
item={item}
collapsed={collapsed}
selected={selected}
onClick={() => {
const url = URL_MAP[item.path];
if (url) {
window.open(url, "_blank", "noreferrer");
} else {
navigate(`/${item.path}`);
}
}}
/>
);
})}
</List>
<Divider sx={{ mt: "auto", borderColor: theme.palette.primary.lowContrast }} />
<AuthFooter
collapsed={collapsed}
accountMenuItems={accountMenuItems}
/>
</Stack>
);
};
export default Sidebar;
@@ -1,14 +0,0 @@
import { Stack, Skeleton } from "@mui/material";
export const SkeletonLayout = () => {
return (
<Stack>
<Skeleton
variant="rectangular"
height={"90vh"}
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -1,51 +0,0 @@
// Components
import { Stack } from "@mui/material";
// Utils
import { useTheme } from "@emotion/react";
const Container = ({ children, direction, backgroundColor, sx }) => {
const theme = useTheme();
let bgColor =
typeof backgroundColor !== "undefined"
? backgroundColor
: theme.palette.background.main;
return (
<Stack
direction={direction}
padding={theme.spacing(8)}
gap={theme.spacing(2)}
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.spacing(4)}
backgroundColor={bgColor}
sx={{ ...sx }}
>
{children}
</Stack>
);
};
export const ColContainer = ({ children, backgroundColor, sx }) => {
return (
<Container
direction="column"
backgroundColor={backgroundColor}
sx={{ ...sx }}
>
{children}
</Container>
);
};
export const RowContainer = ({ children, backgroundColor, sx }) => {
return (
<Container
direction="row"
backgroundColor={backgroundColor}
sx={{ ...sx }}
>
{children}
</Container>
);
};
-149
View File
@@ -1,149 +0,0 @@
import { Stack, Typography } from "@mui/material";
import Image from "../Image/index.jsx";
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js";
/**
* StatBox Component
*
* A reusable component that displays a statistic with a heading and subheading
* in a styled box with a gradient background.
*
* @component
* @param {Object} props - The component props
* @param {string} props.heading - The primary heading/title of the statistic
* @param {string|React.ReactNode} props.subHeading - The value or description of the statistic
* @param {boolean} [props.gradient=false] - Determines if the box should have a gradient background
* @param {string} [props.status] - The status of the statistic
* @param {Object} [props.sx] - Additional custom styling to be applied to the box
*
* @example
* return (
* <StatBox
* heading="Total Users"
* subHeading="1,234"
* sx={{ width: 300 }}
* />
* )
*
* @returns {React.ReactElement} A styled box containing the statistic
*/
const StatBox = ({
img,
icon: Icon,
alt,
heading,
subHeading,
gradient = false,
status = "",
sx,
}) => {
const theme = useTheme();
const { statusToTheme } = useMonitorUtils();
const themeColor = statusToTheme[status];
const statusBoxStyles = gradient
? {
background: `linear-gradient(to bottom right, ${theme.palette[themeColor].main} 30%, ${theme.palette[themeColor].lowContrast} 70%)`,
borderColor: theme.palette[themeColor].lowContrast,
}
: {
background: `linear-gradient(340deg, ${theme.palette.tertiary.main} 10%, ${theme.palette.primary.main} 45%)`,
borderColor: theme.palette.primary.lowContrast,
};
const headingStyles = gradient
? {
color: theme.palette[themeColor].contrastText,
}
: {
color: theme.palette.primary.contrastTextSecondary,
};
const spanFixedStyles = { marginLeft: theme.spacing(2), fontSize: 15, fontWeight: 600 };
const detailTextStyles = gradient
? {
color: theme.palette[themeColor].contrastText,
"& span": {
color: theme.palette[themeColor].contrastText,
...spanFixedStyles,
},
}
: {
color: theme.palette.primary.contrastText,
"& span": {
color: theme.palette.primary.contrastTextTertiary,
...spanFixedStyles,
},
};
const responsiveWidths = {
default: `calc(25% - (3 * ${theme.spacing(8)} / 4))`,
md: `calc(50% - (1 * ${theme.spacing(8)} / 2))`,
};
return (
<Stack
direction="row"
sx={{
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
/* TODO why are we using width and min width here? */
minWidth: 200,
// this is for status box to be reponsive on diff screens
width: responsiveWidths.default, // Default: 4 items per row
[theme.breakpoints.down("md")]: {
width: responsiveWidths.md, // 2 items per row
},
border: 1,
borderStyle: "solid",
borderRadius: 4,
...statusBoxStyles,
"& h2": {
/* TODO font size should come from theme */
fontSize: 13,
fontWeight: 500,
textTransform: "uppercase",
...headingStyles,
},
"& p": {
fontSize: 18,
marginTop: theme.spacing(2),
...detailTextStyles,
},
...sx,
}}
>
{img && (
<Image
src={img}
height={"30px"}
width={"30px"}
alt={alt}
sx={{ marginRight: theme.spacing(8) }}
/>
)}
{Icon && (
<Icon sx={{ width: "30px", height: "30px", marginRight: theme.spacing(8) }} />
)}
<Stack>
<Typography component="h2">{heading}</Typography>
<Typography sx={{ fontWeight: 500 }}>{subHeading}</Typography>
</Stack>
</Stack>
);
};
StatBox.propTypes = {
heading: PropTypes.string,
subHeading: PropTypes.node,
gradient: PropTypes.bool,
status: PropTypes.string,
sx: PropTypes.object,
icon: PropTypes.elementType,
img: PropTypes.node,
alt: PropTypes.string,
};
export default StatBox;
@@ -1,37 +0,0 @@
// Components
import { Stack } from "@mui/material";
import SkeletonLayout from "./skeleton.jsx";
// Utils
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
const StatusBoxes = ({ shouldRender = true, flexWrap = "nowrap", children }) => {
const theme = useTheme();
if (!shouldRender) {
return (
<SkeletonLayout
numBoxes={children?.length ?? 1}
flexWrap={flexWrap}
/>
);
}
return (
<Stack
direction="row"
flexWrap={flexWrap}
gap={theme.spacing(8)}
justifyContent="flex-start"
display="flex"
>
{children}
</Stack>
);
};
StatusBoxes.propTypes = {
shouldRender: PropTypes.bool,
flexWrap: PropTypes.string,
children: PropTypes.node,
};
export default StatusBoxes;
@@ -1,33 +0,0 @@
import { Stack, Skeleton } from "@mui/material";
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
const SkeletonLayout = ({ numBoxes, flexWrap }) => {
const theme = useTheme();
return (
<Stack
direction="row"
flexWrap={flexWrap}
gap={theme.spacing(4)}
mt={theme.spacing(4)}
>
{Array.from({ length: numBoxes }).map((_, index) => {
const width = `${200 / numBoxes}%`;
return (
<Skeleton
variant="rounded"
width={width}
height={100}
key={index}
/>
);
})}
</Stack>
);
};
SkeletonLayout.propTypes = {
flexWrap: PropTypes.string,
numBoxes: PropTypes.number,
};
export default SkeletonLayout;
@@ -1,83 +0,0 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Stack, Typography } from "@mui/material";
/**
*
* @component
* @example
*
* @param {boolean} props.shouldRender - Whether to render the subheader
* @param {string} props.direction - Direction of the subheader
* @param {string} props.headerText - Header text
* @param {number} props.headerLevel - Font characteristic of the header
* @param {string} props.subHeaderText - Subheader text
* @param {number} props.subHeaderLevel - Font characteristic of the subheader
* @param {string} props.alignItems - Align items
* @param {node} props.children - Children
* @returns {JSX.Element} The rendered component
*/
const SubHeader = ({
shouldRender = true,
direction = "row",
headerText,
headerLevel = 1,
subHeaderText,
subHeaderLevel = 1,
alignItems = "center",
children,
...props
}) => {
const theme = useTheme();
if (!shouldRender) {
return null;
}
return (
<Stack
direction={direction}
alignItems={alignItems}
justifyContent="space-between"
{...props}
>
<Stack direction={"column"}>
<Typography
component={`h${headerLevel}`}
variant={`h${headerLevel}`}
mb={theme.spacing(1)}
>
<Typography
component="span"
fontSize="inherit"
color={theme.palette.primary.contrastTextTertiary}
>
{headerText}
</Typography>
</Typography>
<Typography
variant={`body${subHeaderLevel}`}
color={theme.palette.primary.contrastTextTertiary}
>
{subHeaderText}
</Typography>
</Stack>
{children}
</Stack>
);
};
SubHeader.propTypes = {
shouldRender: PropTypes.bool,
direction: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
headerText: PropTypes.string,
headerLevel: PropTypes.number,
subHeaderText: PropTypes.string,
subHeaderLevel: PropTypes.number,
alignItems: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
children: PropTypes.node,
};
export default SubHeader;
-41
View File
@@ -1,41 +0,0 @@
import { Box, useTheme } from "@mui/material";
import { TabList } from "@mui/lab";
import PropTypes from "prop-types";
/**
* CustomTabList component
* @param {string} value - The currently selected tab's value.
* @param {function} onChange - Callback when a different tab is selected.
* @param {React.ReactNode} children - Tab components to render inside the TabList.
* @param {object} props - Additional props passed to the TabList component.
*/
const CustomTabList = ({ value, onChange, children, ...props }) => {
const theme = useTheme();
return (
<Box
sx={{
marginBottom: theme.spacing(12),
borderBottom: `1px solid ${theme.palette.primary.lowContrast}`,
"& .MuiTabs-root": { height: "fit-content", minHeight: "0" },
}}
>
<TabList
value={value}
onChange={onChange}
{...props}
>
{children}
</TabList>
</Box>
);
};
CustomTabList.propTypes = {
value: PropTypes.string,
onChange: PropTypes.func,
children: PropTypes.node,
};
export default CustomTabList;
@@ -1,81 +0,0 @@
import PropTypes from "prop-types";
import { Box, Button } from "@mui/material";
import LeftArrow from "../../../ArrowLeft/index.jsx";
import RightArrow from "../../../ArrowRight/index.jsx";
import { useTheme } from "@emotion/react";
TablePaginationActions.propTypes = {
count: PropTypes.number.isRequired,
page: PropTypes.number.isRequired,
rowsPerPage: PropTypes.number.isRequired,
onPageChange: PropTypes.func.isRequired,
};
/**
* Component for pagination actions (first, previous, next, last).
*
* @component
* @param {Object} props
* @param {number} props.count - Total number of items.
* @param {number} props.page - Current page number.
* @param {number} props.rowsPerPage - Number of rows per page.
* @param {function} props.onPageChange - Callback function to handle page change.
*
* @returns {JSX.Element} Pagination actions component.
*/
function TablePaginationActions({ count, page, rowsPerPage, onPageChange }) {
const handleFirstPageButtonClick = (event) => {
onPageChange(event, 0);
};
const handleBackButtonClick = (event) => {
onPageChange(event, page - 1);
};
const handleNextButtonClick = (event) => {
onPageChange(event, page + 1);
};
const handleLastPageButtonClick = (event) => {
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
};
const theme = useTheme();
return (
<Box sx={{ flexShrink: 0, ml: "24px", display: "flex", gap: theme.spacing(2) }}>
<Button
variant="group"
onClick={handleFirstPageButtonClick}
disabled={page === 0}
aria-label="first page"
>
<LeftArrow type="double" />
</Button>
<Button
variant="group"
onClick={handleBackButtonClick}
disabled={page === 0}
aria-label="previous page"
>
<LeftArrow />
</Button>
<Button
variant="group"
onClick={handleNextButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="next page"
>
<RightArrow />
</Button>
<Button
variant="group"
onClick={handleLastPageButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="last page"
>
<RightArrow type="double" />
</Button>
</Box>
);
}
export { TablePaginationActions };
@@ -1,144 +0,0 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Stack, TablePagination, Typography } from "@mui/material";
import { TablePaginationActions } from "./Actions/index.jsx";
import Icon from "../../Icon";
Pagination.propTypes = {
paginationLabel: PropTypes.string, // Label for the pagination.
itemCount: PropTypes.number, // Total number of items for pagination.
page: PropTypes.number, // Current page index.
rowsPerPage: PropTypes.number, // Number of rows displayed per page.
handleChangePage: PropTypes.func.isRequired, // Function to handle page changes.
handleChangeRowsPerPage: PropTypes.func, // Function to handle changes in rows per page.
};
const ROWS_PER_PAGE_OPTIONS = [5, 10, 15, 25];
// Determine whether pagination buttons should be shown
const shouldShowPaginationButtons = (itemCount, rowsPerPage) => {
return Math.ceil(itemCount / rowsPerPage) > 1;
};
// Empty action component for pagination (no-op component)
const EmptyPaginationActions = () => <div />;
/**
* Pagination component for table navigation with customized styling and behavior.
*
* @param {object} props - Component properties.
* @param {string} props.paginationLabel - Label for the pagination.
* @param {number} props.monitorCount - Total number of monitors to paginate.
* @param {number} props.page - Current page index (0-based).
* @param {number} props.rowsPerPage - Number of rows to display per page.
* @param {function} props.handleChangePage - Callback for handling page changes.
* @param {function} props.handleChangeRowsPerPage - Callback for handling changes to rows per page.
* @returns {JSX.Element} The Pagination component.
*/
function Pagination({
paginationLabel,
itemCount = 0,
page = 0,
rowsPerPage = 5,
handleChangePage,
handleChangeRowsPerPage,
}) {
const theme = useTheme();
const start = page * rowsPerPage + 1;
const end = Math.min(page * rowsPerPage + rowsPerPage, itemCount);
const range = `${start} - ${end}`;
const showPaginationButtons = shouldShowPaginationButtons(itemCount, rowsPerPage);
return (
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
px={theme.spacing(4)}
marginTop={8}
>
<Typography
px={theme.spacing(2)}
variant="body2"
sx={{ opacity: 0.7 }}
>
Showing {range} of {itemCount} {paginationLabel}
</Typography>
<TablePagination
component="div"
count={itemCount}
page={page}
onPageChange={handleChangePage}
rowsPerPage={rowsPerPage}
rowsPerPageOptions={ROWS_PER_PAGE_OPTIONS}
onRowsPerPageChange={handleChangeRowsPerPage}
ActionsComponent={
showPaginationButtons ? TablePaginationActions : EmptyPaginationActions
}
labelRowsPerPage="Rows per page"
labelDisplayedRows={({ page, count }) =>
`Page ${page + 1} of ${Math.max(0, Math.ceil(count / rowsPerPage))}`
}
slotProps={{
select: {
MenuProps: {
keepMounted: true,
disableScrollLock: true,
PaperProps: {
className: "pagination-dropdown",
sx: {
mt: 0,
mb: theme.spacing(2),
},
},
transformOrigin: { vertical: "bottom", horizontal: "left" },
anchorOrigin: { vertical: "top", horizontal: "left" },
sx: {
mt: theme.spacing(-2),
},
},
inputProps: { id: "pagination-dropdown" },
IconComponent: () => (
<Icon
name="ChevronsUpDown"
size={16}
/>
),
sx: {
ml: theme.spacing(4),
mr: theme.spacing(12),
minWidth: theme.spacing(20),
textAlign: "left",
"&.Mui-focused > div": {
backgroundColor: theme.palette.primary.main,
},
"& .MuiSelect-icon": {
// Add this style override
position: "absolute",
right: 0,
top: "50%",
transform: "translateY(-50%)",
},
},
},
}}
sx={{
color: theme.palette.primary.contrastTextSecondary,
"& svg path": {
stroke: theme.palette.primary.contrastTextTertiary,
strokeWidth: 1.3,
},
"& .MuiSelect-select": {
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
},
}}
/>
</Stack>
);
}
export default Pagination;
@@ -1,11 +0,0 @@
export const createHeaderFactory = (getCellSx = () => {}) => {
return ({ id, content, onClick = () => {}, render = () => {} }) => {
return {
id,
content,
onClick,
getCellSx,
render,
};
};
};
-180
View File
@@ -1,180 +0,0 @@
import {
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from "@mui/material";
import Tooltip from "@mui/material/Tooltip";
import SkeletonLayout from "./skeleton.jsx";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
/**
* @typedef {Object} Header
* @property {number|string} id - The unique identifier for the header.
* @property {React.ReactNode} content - The content to display in the header cell.
* @property {Function} onClick - A function to be called when this cell is clicked, receiving the event and row data as an argument.
* @property {Object} getCellSx - Function that takes a row and returns a style object for the table cell.
* @property {Function} render - A function to render the cell content for a given row.
*/
/**
* @typedef {Object} Config
* @property {Function} onRowClick - A function to be called when a row is clicked, receiving the row data as an argument.
* @property {Object} rowSX - Style object for the table row.
*/
/**
* DataTable component renders a table with headers and data.
*
* @param {Object} props - The component props.
* @param {Header[]} props.headers - An array of header objects, each containing an `id`, `content`, and `render` function.
* @param {Array} props.data - An array of data objects, each representing a row.
* @returns {JSX.Element} The rendered table component.
*/
const DataTable = ({
shouldRender = true,
headers = [],
data = [],
config = {
emptyView: "No data",
tooltipContent: null,
onRowClick: () => {},
},
}) => {
const theme = useTheme();
if (!shouldRender) {
return <SkeletonLayout />;
}
if ((headers?.length ?? 0) === 0) {
return "No data";
}
return (
<TableContainer component={Paper}>
<Table
stickyHeader
sx={{
"&.MuiTable-root :is(.MuiTableHead-root, .MuiTableBody-root) :is(th, td)": {
paddingLeft: theme.spacing(8),
},
"& :is(th)": {
backgroundColor: theme.palette.secondary.main,
color: theme.palette.secondary.contrastText,
fontWeight: 600,
},
"& :is(td)": {
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastTextSecondary,
},
"& .MuiTableBody-root .MuiTableRow-root:last-child .MuiTableCell-root": {
borderBottom: "none",
},
}}
>
<TableHead>
<TableRow>
{headers.map((header, index) => (
<TableCell
key={header.id}
align={index === 0 ? "left" : "center"}
>
{header.content}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{(data?.length ?? 0) === 0 ? (
<TableRow>
<TableCell
colSpan={headers.length}
align="center"
>
{config.emptyView}
</TableCell>
</TableRow>
) : (
data.map((row) => {
const key = row.id || row._id || Math.random();
return (
<Tooltip
key={key}
followCursor
enterDelay={500}
enterNextDelay={500}
title={
typeof config.tooltipContent === "function"
? config.tooltipContent(row)
: config.tooltipContent
}
slotProps={{
tooltip: {
sx: {
background: "unset",
},
},
popper: {
modifiers: [
{
name: "offset",
options: {
offset: ({ popper }) => {
return [popper.width / 2 + 20, -popper.height / 8];
},
},
},
],
},
}}
>
<TableRow
sx={config?.rowSX ?? {}}
onClick={config?.onRowClick ? () => config.onRowClick(row) : null}
>
{headers.map((header, index) => {
return (
<TableCell
align={index === 0 ? "left" : "center"}
key={header.id}
onClick={header.onClick ? (e) => header.onClick(e, row) : null}
sx={header.getCellSx ? header.getCellSx(row) : {}}
>
{header.render(row)}
</TableCell>
);
})}
</TableRow>
</Tooltip>
);
})
)}
</TableBody>
</Table>
</TableContainer>
);
};
DataTable.propTypes = {
shouldRender: PropTypes.bool,
headers: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
content: PropTypes.node.isRequired,
render: PropTypes.func.isRequired,
})
).isRequired,
data: PropTypes.array,
config: PropTypes.shape({
onRowClick: PropTypes.func,
rowSX: PropTypes.object,
emptyView: PropTypes.node,
}),
};
export default DataTable;
@@ -1,14 +0,0 @@
import { Skeleton } from "@mui/material";
const TableSkeleton = () => {
return (
<Skeleton
variant="rounded"
width="100%"
height="80%"
flex={1}
/>
);
};
export default TableSkeleton;
+3 -1
View File
@@ -5,7 +5,7 @@ import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Link as RouterLink } from "react-router-dom";
const TextLink = ({ text, linkText, href, target = "_self" }) => {
const TextLink = ({ text, linkText, href, state, target = "_self" }) => {
const theme = useTheme();
return (
@@ -19,6 +19,7 @@ const TextLink = ({ text, linkText, href, target = "_self" }) => {
to={href}
component={RouterLink}
target={target}
state={state}
>
{linkText}
</Link>
@@ -30,6 +31,7 @@ TextLink.propTypes = {
text: PropTypes.string,
linkText: PropTypes.string,
href: PropTypes.string,
state: PropTypes.object,
target: PropTypes.string,
};
@@ -1,98 +0,0 @@
import { useTheme } from "@mui/material";
import "./index.css";
const SunAndMoonIcon = () => {
const theme = useTheme();
return (
<svg
className="sun-and-moon"
aria-hidden="true"
width="24"
height="24"
viewBox="0 0 24 24"
>
<mask
className="moon"
id="moon-mask"
>
<rect
x="0"
y="0"
width="100%"
height="100%"
fill="#fff"
/>
<circle
cx="24"
cy="10"
r="6"
fill="#000"
/>
</mask>
<circle
className="sun"
cx="12"
cy="12"
r="6"
fill={theme.palette.primary.contrastTextSecondary}
mask="url(#moon-mask)"
/>
<g
className="sun-beams"
stroke={theme.palette.primary.contrastTextSecondary}
>
<line
x1="12"
y1="1"
x2="12"
y2="3"
/>
<line
x1="12"
y1="21"
x2="12"
y2="23"
/>
<line
x1="4.22"
y1="4.22"
x2="5.64"
y2="5.64"
/>
<line
x1="18.36"
y1="18.36"
x2="19.78"
y2="19.78"
/>
<line
x1="1"
y1="12"
x2="3"
y2="12"
/>
<line
x1="21"
y1="12"
x2="23"
y2="12"
/>
<line
x1="4.22"
y1="19.78"
x2="5.64"
y2="18.36"
/>
<line
x1="18.36"
y1="5.64"
x2="19.78"
y2="4.22"
/>
</g>
</svg>
);
};
export default SunAndMoonIcon;
@@ -1,64 +0,0 @@
.sun-and-moon > :is(.moon, .sun, .sun-beams) {
transform-origin: center;
}
.theme-toggle .sun-and-moon > .sun-beams {
stroke-width: 2px;
}
.theme-dark .sun-and-moon > .sun {
transform: scale(1.75);
}
.theme-dark .sun-and-moon > .sun-beams {
opacity: 0;
}
.theme-dark .sun-and-moon > .moon > circle {
transform: translateX(-7px);
}
@supports (cx: 1) {
.theme-dark .sun-and-moon > .moon > circle {
cx: 17;
transform: translateX(0);
}
}
@media (prefers-reduced-motion: no-preference) {
.sun-and-moon > .sun {
transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
.sun-and-moon > .sun-beams {
transition:
transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55),
opacity 0.5s cubic-bezier(0.25, 0.1, 0.25, 1);
}
.sun-and-moon .moon > circle {
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
@supports (cx: 1) {
.sun-and-moon .moon > circle {
transition: cx 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
}
.theme-dark .sun-and-moon > .sun {
transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
transition-duration: 0.25s;
transform: scale(1.75);
}
.theme-dark .sun-and-moon > .sun-beams {
transition-duration: 0.15s;
transform: rotateZ(-25deg);
}
.theme-dark .sun-and-moon > .moon > circle {
transition-duration: 0.5s;
transition-delay: 0.25s;
}
}
@@ -1,53 +0,0 @@
/**
* ThemeSwitch Component
* Dark and Light Theme Switch
* Original Code: https://web.dev/patterns/theming/theme-switch
* License: Apache License 2.0
* Copyright © Google LLC
*
* This code has been adapted for use in this project.
* Apache License: https://www.apache.org/licenses/LICENSE-2.0
*/
import { IconButton } from "@mui/material";
import SunAndMoonIcon from "./SunAndMoonIcon.jsx";
import { useDispatch, useSelector } from "react-redux";
import { setMode } from "../../../Features/UI/uiSlice.js";
import "./index.css";
import { useTranslation } from "react-i18next";
const ThemeSwitch = ({ width = 48, height = 48, color }) => {
const mode = useSelector((state) => state.ui.mode);
const dispatch = useDispatch();
const { t } = useTranslation();
const toggleTheme = () => {
dispatch(setMode(mode === "light" ? "dark" : "light"));
};
return (
<IconButton
id="theme-toggle"
title={t("common.buttons.toggleTheme")}
className={`theme-${mode}`}
aria-label="auto"
aria-live="polite"
onClick={toggleTheme}
sx={{
width,
height,
display: "flex",
alignItems: "center",
justifyContent: "center",
"& svg >:is(circle, g)": {
fill: color,
stroke: color,
},
}}
>
<SunAndMoonIcon />
</IconButton>
);
};
export default ThemeSwitch;
@@ -0,0 +1,48 @@
import { Avatar as MuiAvatar } from "@mui/material";
import { useSelector } from "react-redux";
import { useEffect, useState } from "react";
import { useTheme } from "@mui/material";
import type { RootState } from "@/Types/state";
interface AvatarProps {
src?: string;
small?: boolean;
sx?: object;
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
}
export const Avatar = ({ src, small, sx, onClick = () => {} }: AvatarProps) => {
const { user } = useSelector((state: RootState) => state.auth);
const theme = useTheme();
if (!user) return null;
const style = small ? { width: 32, height: 32 } : { width: 64, height: 64 };
const [image, setImage] = useState<string>();
useEffect(() => {
if (user.avatarImage) {
setImage(`data:image/png;base64,${user.avatarImage}`);
}
}, [user?.avatarImage]);
return (
<MuiAvatar
onClick={onClick}
alt={`${user?.firstName} ${user?.lastName}`}
src={src ? src : user?.avatarImage ? image : undefined}
sx={{
color: theme.palette.primary.contrastText,
fontSize: small ? "16px" : "22px",
fontWeight: 400,
backgroundColor: theme.palette.primary.main,
display: "inline-flex",
...style,
...sx,
}}
>
{user.firstName?.charAt(0)}
{user.lastName?.charAt(0) || ""}
</MuiAvatar>
);
};
@@ -1,4 +1,6 @@
import Logo from "@/assets/icons/checkmate-icon.svg?react";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Alert from "@mui/material/Alert";
import Link from "@mui/material/Link";
import {
@@ -9,6 +11,7 @@ import {
} from "@/Components/v2/design-elements/Fallback";
import { Breadcrumb } from "@/Components/v2/design-elements/Breadcrumb";
import CircularProgress from "@mui/material/CircularProgress";
import { HeaderAuthControls } from "@/Pages/Auth/components/HeaderAuthControls";
import type { StackProps } from "@mui/material/Stack";
import { useTheme } from "@mui/material/styles";
@@ -100,7 +103,7 @@ export const BasePage = ({
interface BasePageWithStatesProps extends StackProps {
loading: boolean;
error: any;
items: any[];
totalCount: number;
bullets: string[] | unknown;
page: string;
actionButtonText: string;
@@ -111,7 +114,7 @@ interface BasePageWithStatesProps extends StackProps {
export const BasePageWithStates = ({
loading,
error,
items,
totalCount,
page,
bullets,
actionButtonText,
@@ -119,9 +122,9 @@ export const BasePageWithStates = ({
children,
...props
}: BasePageWithStatesProps) => {
const showLoading = loading && (!items || items.length === 0);
const showLoading = loading && totalCount === 0;
if (isEmpty(items)) {
if (!loading && totalCount === 0) {
return (
<EmptyFallback
bullets={bullets}
@@ -146,23 +149,17 @@ export const BasePageWithStates = ({
interface MonitorBasePageWithStatesProps extends StackProps {
loading: boolean;
error: any;
items: any[];
totalCount: number;
page: string;
actionLink?: string;
children: React.ReactNode;
priorityFallback?: React.ReactNode;
}
const isEmpty = (items: any[]) => {
if (!items) return true;
if (Array.isArray(items) && items.length === 0) return true;
return false;
};
export const MonitorBasePageWithStates = ({
loading,
error,
items,
totalCount,
page,
actionLink,
children,
@@ -171,7 +168,7 @@ export const MonitorBasePageWithStates = ({
}: MonitorBasePageWithStatesProps) => {
const { t } = useTranslation();
const showLoading = loading && (!items || items.length === 0);
const showLoading = loading && totalCount === 0;
if (priorityFallback) {
return (
@@ -180,12 +177,12 @@ export const MonitorBasePageWithStates = ({
error={error}
{...props}
>
{priorityFallback}
<Stack height={"100%"}>{priorityFallback}</Stack>
</BasePage>
);
}
if (!loading && isEmpty(items)) {
if (!loading && totalCount === 0) {
return (
<EmptyMonitorFallback
page={page}
@@ -207,3 +204,61 @@ export const MonitorBasePageWithStates = ({
</BasePage>
);
};
interface BaseAuthPageProps extends React.PropsWithChildren, StackProps {
title: string;
subtitle: string;
}
export const BaseAuthPage = ({
title,
subtitle,
children,
...props
}: BaseAuthPageProps) => {
const theme = useTheme();
return (
<BasePage
breadcrumbOverride={[]}
gap={theme.spacing(8)}
alignItems={"center"}
justifyContent={"center"}
minHeight="100vh"
position={"relative"}
{...props}
>
<HeaderAuthControls
hideLogo
py={theme.spacing(4)}
position={"absolute"}
top={0}
left={0}
/>
<Box width={{ xs: 60, sm: 70, md: 80 }}>
<Logo
width={"100%"}
height={"100%"}
/>
</Box>
<Stack alignItems={"center"}>
<Typography
variant="h1"
mb={theme.spacing(2)}
>
{title}
</Typography>
<Typography variant="h2">{subtitle}</Typography>
</Stack>
<Stack
gap={theme.spacing(8)}
width={{
xs: "80%",
md: "25%",
lg: "15%",
}}
>
{children}
</Stack>
</BasePage>
);
};
@@ -27,6 +27,7 @@ export const BulletPointCheck = ({
<Check
size={16}
strokeWidth={1.5}
color={variant === "info" ? theme.palette.text.secondary : colors[variant]}
style={{
flexShrink: 0,
}}
@@ -1,3 +1,5 @@
import { BaseChart } from "@/Components/v2/design-elements";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
@@ -104,3 +106,50 @@ export const Gauge = ({
</Box>
);
};
export const DetailGauge = ({
title,
progress,
upperLabel,
upperValue,
lowerLabel,
lowerValue,
}: {
title: string;
progress: number;
upperLabel?: string;
upperValue?: string | number;
lowerLabel?: string;
lowerValue?: string | number;
}) => {
const theme = useTheme();
return (
<BaseChart
icon={null}
title={title}
maxWidth={225}
>
<Stack
alignItems={"center"}
mb={theme.spacing(4)}
gap={theme.spacing(4)}
>
<Gauge progress={progress} />
</Stack>
<Stack
direction={"row"}
justifyContent={"space-between"}
>
<Typography>{upperLabel}</Typography>
<Typography>{upperValue}</Typography>
</Stack>
<Stack
direction={"row"}
justifyContent={"space-between"}
>
<Typography>{lowerLabel}</Typography>
<Typography>{lowerValue}</Typography>
</Stack>
</BaseChart>
);
};
@@ -4,6 +4,7 @@ interface IconProps {
icon: LucideIcon;
size?: number;
strokeWidth?: number;
stroke?: string;
}
const Icon = ({ icon: Icon, size = 20, strokeWidth = 1.5 }: IconProps) => {
@@ -23,7 +23,6 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
overflow={"hidden"}
textOverflow={"ellipsis"}
whiteSpace={"nowrap"}
maxWidth={isSmall ? "100%" : "calc((100vw - var(--env-var-width-2)) / 2)"}
>
{monitor.name}
</Typography>
@@ -40,7 +39,6 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
overflow={"hidden"}
textOverflow={"ellipsis"}
whiteSpace={"nowrap"}
maxWidth={isSmall ? "100%" : "calc((100vw - var(--env-var-width-2)) / 2)"}
>
{formatUrl(monitor?.url)}
</Typography>
Loaded 100 of 335 files, more files were not shown because too many files have changed in this diff. Show more