Compare commits
518
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98943ad461 | ||
|
|
c670653ab3 | ||
|
|
e75606aad3 | ||
|
|
07b0483352 | ||
|
|
8555e7c122 | ||
|
|
734ec41ca0 | ||
|
|
5152336591 | ||
|
|
78b920aaf6 | ||
|
|
2d84fae470 | ||
|
|
97e4b105a2 | ||
|
|
a053386f4a | ||
|
|
e0991e28e5 | ||
|
|
0f26648855 | ||
|
|
6a0f3426b3 | ||
|
|
16e8035299 | ||
|
|
31b09c316a | ||
|
|
8f2072e497 | ||
|
|
514570da78 | ||
|
|
ea68e660fc | ||
|
|
527543d79f | ||
|
|
1c390a8baf | ||
|
|
43ac5b57cf | ||
|
|
1558b27497 | ||
|
|
9caa6c830f | ||
|
|
ea115ede53 | ||
|
|
8470631162 | ||
|
|
597732e109 | ||
|
|
9974fa4010 | ||
|
|
b0686c0576 | ||
|
|
a148f56b22 | ||
|
|
3b25b37c22 | ||
|
|
1e04345aa0 | ||
|
|
174c3cd47a | ||
|
|
0e149fa477 | ||
|
|
5b4a99af3d | ||
|
|
2ecdc2096e | ||
|
|
6e66a18535 | ||
|
|
bd6aa0fa3a | ||
|
|
4e678a4ce7 | ||
|
|
5999315791 | ||
|
|
0c8b8a9358 | ||
|
|
dfe62bd301 | ||
|
|
747ff824cc | ||
|
|
f12c544751 | ||
|
|
278ced2a6c | ||
|
|
88d539b2a4 | ||
|
|
f0b152fc8f | ||
|
|
dc0e198a8e | ||
|
|
0a5067ef46 | ||
|
|
4f0017c031 | ||
|
|
f0b2292f0a | ||
|
|
d74d79c332 | ||
|
|
1a096b66f7 | ||
|
|
7cb866ce4a | ||
|
|
c54582ca70 | ||
|
|
362034ba26 | ||
|
|
b536099d03 | ||
|
|
04fef2ef75 | ||
|
|
a89e5ed565 | ||
|
|
ca153b65a9 | ||
|
|
cc188b2ba3 | ||
|
|
18018ceef1 | ||
|
|
189f309631 | ||
|
|
97dd3ec9cb | ||
|
|
b4c929963f | ||
|
|
4ecc737438 | ||
|
|
bc58f6e08e | ||
|
|
4f3bcd4624 | ||
|
|
d469590ef3 | ||
|
|
35eba6897a | ||
|
|
667dc55fc8 | ||
|
|
f2d2687d2a | ||
|
|
4e3e580179 | ||
|
|
4080ac8c2c | ||
|
|
0a408f8cfd | ||
|
|
e3921beac4 | ||
|
|
4c284493ba | ||
|
|
0fcceeb7a6 | ||
|
|
f799d46025 | ||
|
|
a80df60fc1 | ||
|
|
cbec0fa065 | ||
|
|
40e5550e0f | ||
|
|
90fd9930db | ||
|
|
f10f7385b0 | ||
|
|
2d85abf030 | ||
|
|
3c0c470ca3 | ||
|
|
4c5cae8722 | ||
|
|
8650f30d12 | ||
|
|
09e37f4356 | ||
|
|
6bc8a521c9 | ||
|
|
a1e6eaa9d0 | ||
|
|
bbb61a6f88 | ||
|
|
c1d2041482 | ||
|
|
7aa05c7f85 | ||
|
|
539866e119 | ||
|
|
6f0ff1583e | ||
|
|
12b61d17f2 | ||
|
|
7426418629 | ||
|
|
3566da7515 | ||
|
|
b451489bd8 | ||
|
|
d4a0489275 | ||
|
|
080c91284d | ||
|
|
dadc74373c | ||
|
|
6fdea04416 | ||
|
|
67963823d9 | ||
|
|
ab12a9bd3a | ||
|
|
3b25c50247 | ||
|
|
63e42b7276 | ||
|
|
090d002578 | ||
|
|
ae7524f507 | ||
|
|
54d71bef61 | ||
|
|
a77c74b17c | ||
|
|
f2363a7a09 | ||
|
|
69f95723f1 | ||
|
|
1fefb038f0 | ||
|
|
85190992c7 | ||
|
|
efe87b599f | ||
|
|
ed1bcf97da | ||
|
|
56ead03416 | ||
|
|
a912a5ebb8 | ||
|
|
0fd4173816 | ||
|
|
75d3644b36 | ||
|
|
853e21623e | ||
|
|
57c3bf3240 | ||
|
|
6be1eb0a3e | ||
|
|
9d70623e0f | ||
|
|
43fe66022b | ||
|
|
294fb13005 | ||
|
|
8028cab487 | ||
|
|
8160ab4a3c | ||
|
|
6063532c79 | ||
|
|
83b628c8f9 | ||
|
|
88a91022c3 | ||
|
|
5822a363a9 | ||
|
|
464b16ec9d | ||
|
|
d54c90d511 | ||
|
|
3a5783fe91 | ||
|
|
479f22ac69 | ||
|
|
30202b2249 | ||
|
|
888910abb2 | ||
|
|
874d1b277a | ||
|
|
f34520cdbf | ||
|
|
f420044b64 | ||
|
|
53d5ae2251 | ||
|
|
bcc57c7135 | ||
|
|
6b67f4977f | ||
|
|
87255071f4 | ||
|
|
cbf1f97591 | ||
|
|
7a881fda03 | ||
|
|
50fd66ef56 | ||
|
|
f9b677b1ad | ||
|
|
b7a6d50c73 | ||
|
|
04bd7f0e2e | ||
|
|
2e7aba788d | ||
|
|
e1665b29af | ||
|
|
17450e2537 | ||
|
|
270293a2b5 | ||
|
|
a7c5b0c7fb | ||
|
|
e804c35c3a | ||
|
|
5c4133097d | ||
|
|
85f93cd6d2 | ||
|
|
d4db32e392 | ||
|
|
791b9fc7a2 | ||
|
|
206c5577a6 | ||
|
|
cbd0530d85 | ||
|
|
45ef2438de | ||
|
|
ec818eaacb | ||
|
|
91bb60f2ff | ||
|
|
443ad46fa7 | ||
|
|
f685c9cd5d | ||
|
|
0263c17618 | ||
|
|
5a12e36c7e | ||
|
|
67da6c61cf | ||
|
|
f538f61464 | ||
|
|
f6971ede69 | ||
|
|
e69283e3ed | ||
|
|
084eed85bf | ||
|
|
9a42fb031f | ||
|
|
943e88c54d | ||
|
|
24ce64b439 | ||
|
|
841d25ce3e | ||
|
|
84e4ea7b64 | ||
|
|
4b92dbd19b | ||
|
|
5eb1d54d65 | ||
|
|
a0aee182de | ||
|
|
2dca5ccb67 | ||
|
|
348eea5475 | ||
|
|
2a3683c74a | ||
|
|
806d7ae456 | ||
|
|
cac502573f | ||
|
|
830318edc3 | ||
|
|
86a55e3cfe | ||
|
|
796960aeb2 | ||
|
|
a25ff03b71 | ||
|
|
22ae6b4be6 | ||
|
|
0b5e28c296 | ||
|
|
c5c91e28f8 | ||
|
|
3ba3e4b865 | ||
|
|
77fa394004 | ||
|
|
c0ba278e47 | ||
|
|
73f41906b0 | ||
|
|
cb5af2741d | ||
|
|
af4ef87966 | ||
|
|
a154ad11b5 | ||
|
|
1bfd1c9119 | ||
|
|
fc98192bcf | ||
|
|
b1db74d96f | ||
|
|
c1258d3cdf | ||
|
|
6ffa5ac648 | ||
|
|
6395e8a10e | ||
|
|
2852d2cdce | ||
|
|
363baef7b1 | ||
|
|
a1973a6d2b | ||
|
|
12f02d6eea | ||
|
|
a6c291c8ef | ||
|
|
227dc9e7e8 | ||
|
|
38ef9ccad3 | ||
|
|
353b97e7d3 | ||
|
|
9b871ffb52 | ||
|
|
cb392eaaba | ||
|
|
dcf5a77923 | ||
|
|
ec03addba6 | ||
|
|
eefdb8d253 | ||
|
|
e539f40612 | ||
|
|
c18159afd3 | ||
|
|
5596e45ff3 | ||
|
|
bde13f4fa7 | ||
|
|
7f1c996632 | ||
|
|
396d5b369b | ||
|
|
bb8cc8b464 | ||
|
|
66efa25e2b | ||
|
|
7bef6dc0e6 | ||
|
|
1c341a4e3d | ||
|
|
f41f2322cf | ||
|
|
b653f26c96 | ||
|
|
ece1dadb78 | ||
|
|
1f003c4d48 | ||
|
|
5a468d717a | ||
|
|
975d134dd8 | ||
|
|
7a66b5310b | ||
|
|
fcfad945a0 | ||
|
|
6b4032ecfe | ||
|
|
41d6c092d4 | ||
|
|
f9e366e181 | ||
|
|
07705e9823 | ||
|
|
c48049e0af | ||
|
|
f3ba8c9815 | ||
|
|
f88e4ee6f4 | ||
|
|
53f420cf56 | ||
|
|
589825c1c5 | ||
|
|
300b1b2238 | ||
|
|
6efc05879a | ||
|
|
20dccb140e | ||
|
|
8dfedb8a52 | ||
|
|
d48cdaca9c | ||
|
|
36a53ee80b | ||
|
|
4400ca703d | ||
|
|
c1d0cb568d | ||
|
|
81e73e5adf | ||
|
|
7a84d06a79 | ||
|
|
63d2bb97ee | ||
|
|
0af7cfe714 | ||
|
|
d190f5ab02 | ||
|
|
8cfe1d3c1a | ||
|
|
1bf4c75418 | ||
|
|
7771a0ce35 | ||
|
|
fcd9e78367 | ||
|
|
6f9fefddd2 | ||
|
|
5731283c46 | ||
|
|
420ff292d9 | ||
|
|
290ee7d2df | ||
|
|
50c644231e | ||
|
|
5729b0a263 | ||
|
|
4f3f5408ef | ||
|
|
de7038d6c3 | ||
|
|
4119189c71 | ||
|
|
fd66affea8 | ||
|
|
839dc201df | ||
|
|
bbb0716251 | ||
|
|
bbdfc00eb9 | ||
|
|
ba498534e6 | ||
|
|
f51bf36ddd | ||
|
|
716479c5bb | ||
|
|
f9c85e2d76 | ||
|
|
e26f3facf1 | ||
|
|
fd5f493345 | ||
|
|
e5bb736530 | ||
|
|
d71d16ecd0 | ||
|
|
389716fa77 | ||
|
|
1bcb2ddf91 | ||
|
|
b61077bd3f | ||
|
|
1cffd51bd3 | ||
|
|
451e352458 | ||
|
|
a07e41d8f1 | ||
|
|
90e9b41973 | ||
|
|
1819df5117 | ||
|
|
6380dcc6ba | ||
|
|
6cb45ea6de | ||
|
|
67f74dd7de | ||
|
|
8f58ceafab | ||
|
|
c7aa45cd77 | ||
|
|
d0727f91dd | ||
|
|
967b2b200b | ||
|
|
d71c7c6ee1 | ||
|
|
6daa79f0ed | ||
|
|
93a5eff562 | ||
|
|
11c95f885d | ||
|
|
174665e70a | ||
|
|
64de01437e | ||
|
|
a64885e609 | ||
|
|
e354fa7ebf | ||
|
|
04cf47544a | ||
|
|
e1dfd98840 | ||
|
|
4ce4a124d6 | ||
|
|
f564636cb4 | ||
|
|
3208cc9e2c | ||
|
|
79dfa886f1 | ||
|
|
8af8efbe3b | ||
|
|
71bd1fa28f | ||
|
|
14a08a303b | ||
|
|
4645bde025 | ||
|
|
a2569c50c7 | ||
|
|
f7ae11997d | ||
|
|
0b7a41dc78 | ||
|
|
d42d5f2325 | ||
|
|
369d16c7c3 | ||
|
|
6e8172c126 | ||
|
|
794018a6ab | ||
|
|
8d6bc7bc0b | ||
|
|
30631d6ff8 | ||
|
|
56c5ad419a | ||
|
|
b852293447 | ||
|
|
d877ecf061 | ||
|
|
73e90bfab6 | ||
|
|
a55163de56 | ||
|
|
f5c6abad1b | ||
|
|
3dbbe035a8 | ||
|
|
9904626526 | ||
|
|
61f3aa1d1c | ||
|
|
a0a8ceb0db | ||
|
|
c2dece862c | ||
|
|
69018a4944 | ||
|
|
363d89e4f4 | ||
|
|
f8bb01190e | ||
|
|
00cf0b6521 | ||
|
|
a11d551a99 | ||
|
|
b35be1ecfc | ||
|
|
9d64de57b3 | ||
|
|
de6c4d9c3e | ||
|
|
e0e79fb0da | ||
|
|
3339b6b67f | ||
|
|
b511cde849 | ||
|
|
ce322a8e28 | ||
|
|
d25ca473de | ||
|
|
5292ca8988 | ||
|
|
c98cba2eef | ||
|
|
6b89db131d | ||
|
|
995ae5252d | ||
|
|
e6c268f14e | ||
|
|
5aef405414 | ||
|
|
a5112a7955 | ||
|
|
5404b65ce2 | ||
|
|
34dfe33c29 | ||
|
|
1e24c8841e | ||
|
|
0c04c06781 | ||
|
|
7441169a0e | ||
|
|
3520e5adf4 | ||
|
|
227e0128f9 | ||
|
|
2359255ef7 | ||
|
|
b074f2401f | ||
|
|
eee3caf021 | ||
|
|
f9848e4e1e | ||
|
|
8bd608bee0 | ||
|
|
ea589d6be4 | ||
|
|
5a0078cf92 | ||
|
|
f4a5bd65ad | ||
|
|
4766594413 | ||
|
|
9962866ba5 | ||
|
|
82cc289f53 | ||
|
|
117c22fae4 | ||
|
|
e0cabdb4fc | ||
|
|
155a2577d0 | ||
|
|
d627a25b40 | ||
|
|
b8a5af9c10 | ||
|
|
5b43d9d173 | ||
|
|
801b716a03 | ||
|
|
fdc3983328 | ||
|
|
0cf85f8b3b | ||
|
|
0ea4a5024a | ||
|
|
09513696ee | ||
|
|
b0da1f4259 | ||
|
|
5b96075c03 | ||
|
|
36e02e0a9a | ||
|
|
9fa0113f2b | ||
|
|
371e8cc154 | ||
|
|
7b4286646b | ||
|
|
2a8d2cb06a | ||
|
|
147eff3ede | ||
|
|
81c7c992e2 | ||
|
|
35488fed3e | ||
|
|
760abe69ed | ||
|
|
d242382abf | ||
|
|
ef61a63da0 | ||
|
|
3fa370765e | ||
|
|
3b5e793f8f | ||
|
|
b34dbec3cf | ||
|
|
90eee474d2 | ||
|
|
63416ceb4b | ||
|
|
eb8c9bea76 | ||
|
|
63b4779961 | ||
|
|
d2040495d6 | ||
|
|
f436e15569 | ||
|
|
952c70fca4 | ||
|
|
4957209307 | ||
|
|
cc9850c975 | ||
|
|
c6ac48b4ff | ||
|
|
82584d85c7 | ||
|
|
ffd67150eb | ||
|
|
44079f2eb8 | ||
|
|
f03ded4109 | ||
|
|
fa520e5f88 | ||
|
|
e9fbb472f4 | ||
|
|
de236f03eb | ||
|
|
4f043fe1d4 | ||
|
|
102bd0a5f8 | ||
|
|
118372630e | ||
|
|
27dea6a08e | ||
|
|
dd259fea89 | ||
|
|
9af25a19e5 | ||
|
|
6101fe1bfa | ||
|
|
795146c79b | ||
|
|
a69091bb28 | ||
|
|
6160806963 | ||
|
|
c1b2bda623 | ||
|
|
c0e4893f96 | ||
|
|
476ff39a17 | ||
|
|
f467ef1411 | ||
|
|
36b02e5e66 | ||
|
|
f6fd8729c3 | ||
|
|
cdfc87da09 | ||
|
|
427e5a18ea | ||
|
|
f696b4ef9b | ||
|
|
69d050ff61 | ||
|
|
7afdcb8ebc | ||
|
|
3cf7243c93 | ||
|
|
4ed0033a41 | ||
|
|
bafd82a691 | ||
|
|
adb2a383cf | ||
|
|
e91a54b75c | ||
|
|
33dfedb4d3 | ||
|
|
dde54a4e45 | ||
|
|
5f17520ba0 | ||
|
|
bebb5d6942 | ||
|
|
7c9410f9d7 | ||
|
|
348ca0dd02 | ||
|
|
1d36f40caf | ||
|
|
9006707e57 | ||
|
|
be82a7ee2d | ||
|
|
fd33b903ed | ||
|
|
ab25531758 | ||
|
|
f7f764fd68 | ||
|
|
3b0326457f | ||
|
|
7d6bc4b2dc | ||
|
|
c2487f0134 | ||
|
|
a86588f080 | ||
|
|
4e9839e2e5 | ||
|
|
487f6b222d | ||
|
|
391f42ee53 | ||
|
|
cff5508614 | ||
|
|
a0ebf7d224 | ||
|
|
ec9c79fc94 | ||
|
|
3e8d4cb49b | ||
|
|
288ac08cd6 | ||
|
|
ba35230919 | ||
|
|
048e53ea15 | ||
|
|
e498bd60b1 | ||
|
|
a4cf53ee15 | ||
|
|
3a77c6d7fa | ||
|
|
b47f3710fe | ||
|
|
9c92542653 | ||
|
|
173a4e17d2 | ||
|
|
70b197675e | ||
|
|
11dcccc085 | ||
|
|
e3e08da259 | ||
|
|
6447c315ce | ||
|
|
cb5b8e0673 | ||
|
|
f1a6afb860 | ||
|
|
5c52a208b9 | ||
|
|
4fdc0bde5c | ||
|
|
3d4ebe9588 | ||
|
|
88bb8ef2d3 | ||
|
|
64dc381e0f | ||
|
|
af82dec2ad | ||
|
|
62ea7b0e5b | ||
|
|
8747c38f40 | ||
|
|
d633fc5ccb | ||
|
|
0d7a35367f | ||
|
|
af141ec772 | ||
|
|
5784fec3d1 | ||
|
|
7436c2b5df | ||
|
|
2055943876 | ||
|
|
95cd33ac06 | ||
|
|
2a591ee503 | ||
|
|
f5aa24d77e | ||
|
|
7874439e4c | ||
|
|
7140d436a5 | ||
|
|
ba0c89fc6c | ||
|
|
d91e87f484 | ||
|
|
bf15018173 | ||
|
|
c60d07c3a0 | ||
|
|
54ecf02d9a | ||
|
|
1ab31d19f3 | ||
|
|
0f764ca084 | ||
|
|
ed80837810 | ||
|
|
cf67c82f5e | ||
|
|
5a7b51bfec | ||
|
|
86a2171988 | ||
|
|
1c01dcaf31 |
No files matched your search
@@ -3,3 +3,4 @@
|
|||||||
.VSCodeCounter
|
.VSCodeCounter
|
||||||
*.sh
|
*.sh
|
||||||
mongo
|
mongo
|
||||||
|
node_modules/
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
echo "Running lint-staged..."
|
||||||
|
cd client && npx lint-staged && cd ..
|
||||||
|
cd server && npx lint-staged && cd ..
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Checkmate is an open-source uptime and infrastructure monitoring application. It monitors server hardware, uptime, response times, and incidents with real-time alerts. The companion agent [Capture](https://github.com/bluewave-labs/capture) provides infrastructure metrics (CPU, RAM, disk, temperature).
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Client (React/Vite)
|
||||||
|
```bash
|
||||||
|
cd client
|
||||||
|
npm install
|
||||||
|
npm run dev # Start dev server at http://localhost:5173
|
||||||
|
npm run build # TypeScript check + production build
|
||||||
|
npm run lint # ESLint (strict, max-warnings 0)
|
||||||
|
npm run format # Prettier formatting
|
||||||
|
npm run format-check # Check formatting
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server (Node.js/Express)
|
||||||
|
```bash
|
||||||
|
cd server
|
||||||
|
npm install
|
||||||
|
npm run dev # Start with hot-reload (nodemon + tsx) at http://localhost:52345
|
||||||
|
npm run build # TypeScript compile + path alias resolution
|
||||||
|
npm run test # Run Mocha tests with c8 coverage
|
||||||
|
npm run lint # ESLint v9
|
||||||
|
npm run lint-fix # Auto-fix lint issues
|
||||||
|
npm run format # Prettier formatting
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Development
|
||||||
|
```bash
|
||||||
|
cd docker/dev
|
||||||
|
./build_images.sh
|
||||||
|
docker run -d -p 27017:27017 -v uptime_mongo_data:/data/db --name uptime_database_mongo mongo:6.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Setup
|
||||||
|
|
||||||
|
### Server `.env` (minimum required)
|
||||||
|
```env
|
||||||
|
CLIENT_HOST="http://localhost:5173"
|
||||||
|
JWT_SECRET="my_secret_key_change_this"
|
||||||
|
DB_CONNECTION_STRING="mongodb://localhost:27017/uptime_db"
|
||||||
|
TOKEN_TTL="99d"
|
||||||
|
ORIGIN="localhost"
|
||||||
|
LOG_LEVEL="debug"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client `.env`
|
||||||
|
```env
|
||||||
|
VITE_APP_API_BASE_URL="http://localhost:52345/api/v1"
|
||||||
|
VITE_APP_LOG_LEVEL="debug"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Monorepo Structure
|
||||||
|
- `/client` - React 18 + TypeScript + Vite + MUI frontend
|
||||||
|
- `/server` - Node.js 20+ + Express + TypeScript backend
|
||||||
|
- `/docker` - Multi-environment Docker configs (dev, staging, prod, arm, mono)
|
||||||
|
|
||||||
|
### Backend Layers
|
||||||
|
```
|
||||||
|
server/src/
|
||||||
|
├── controllers/ # Route handlers (authController, monitorController, etc.)
|
||||||
|
├── service/ # Business logic
|
||||||
|
│ ├── business/ # Core monitoring logic
|
||||||
|
│ ├── infrastructure/ # Server/system utilities
|
||||||
|
│ └── system/ # App-level settings
|
||||||
|
├── db/
|
||||||
|
│ ├── models/ # Mongoose schemas (Monitor, Check, Incident, User, etc.)
|
||||||
|
│ ├── migration/ # Database migrations (run on startup)
|
||||||
|
│ └── modules/ # Database-specific modules
|
||||||
|
├── middleware/v1/ # verifyJWT, rateLimiter, sanitization, responseHandler
|
||||||
|
├── routes/v1/ # API route definitions
|
||||||
|
├── validation/ # Joi input validation schemas
|
||||||
|
└── repositories/ # Data access layer
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend Structure
|
||||||
|
```
|
||||||
|
client/src/
|
||||||
|
├── Components/ # Reusable UI components
|
||||||
|
├── Pages/ # Page components (Auth, Uptime, Infrastructure, Incidents, etc.)
|
||||||
|
├── Features/ # Redux slices (Auth, UI)
|
||||||
|
├── Hooks/ # Custom React hooks
|
||||||
|
├── Utils/ # Utilities (NetworkService.js is main API client)
|
||||||
|
├── Validation/ # Input validation
|
||||||
|
└── locales/ # i18n translations
|
||||||
|
```
|
||||||
|
|
||||||
|
### API
|
||||||
|
- Base URL: `/api/v1`
|
||||||
|
- Documentation: `http://localhost:52345/api-docs` (Swagger UI)
|
||||||
|
- OpenAPI spec: `/server/openapi.json`
|
||||||
|
|
||||||
|
### Key Technologies
|
||||||
|
- **State Management**: Redux Toolkit + Redux-Persist
|
||||||
|
- **Data Fetching**: SWR + Axios
|
||||||
|
- **Database**: MongoDB with Mongoose ODM
|
||||||
|
- **Queue/Cache**: Redis + BullMQ + Pulse (cron scheduling)
|
||||||
|
- **i18n**: i18next + react-i18next (translations via PoEditor)
|
||||||
|
|
||||||
|
## Code Conventions
|
||||||
|
|
||||||
|
### Internationalization
|
||||||
|
All user-facing strings must use the translation function:
|
||||||
|
```javascript
|
||||||
|
t('your.key') // Never hardcode UI strings
|
||||||
|
```
|
||||||
|
|
||||||
|
### Branching
|
||||||
|
- Always branch from `develop` (not master)
|
||||||
|
- Use descriptive names: `feat/add-alerts`, `fix/login-error`
|
||||||
|
- PRs target `develop` branch
|
||||||
|
|
||||||
|
### Formatting
|
||||||
|
- **Client**: Prettier with `printWidth: 90`, tabs, double quotes
|
||||||
|
- **Server**: Prettier with `printWidth: 150`, tabs, double quotes
|
||||||
|
- Both use ESLint with strict settings
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
Server tests use Mocha + Chai + Sinon:
|
||||||
|
```bash
|
||||||
|
npm test # Run all tests with coverage
|
||||||
|
npm test -- --grep "pattern" # Run specific tests
|
||||||
|
```
|
||||||
|
Test files: `server/tests/**/*.test.js`
|
||||||
|
|
||||||
|
## Database Models
|
||||||
|
|
||||||
|
Key Mongoose models in `/server/src/db/models/`:
|
||||||
|
- **Monitor** - Monitoring configuration (website, infrastructure, port, etc.)
|
||||||
|
- **Check** - Individual monitoring check results
|
||||||
|
- **Incident** - Downtime incidents
|
||||||
|
- **User** - User accounts
|
||||||
|
- **Team** - Team/workspace management
|
||||||
|
- **StatusPage** - Public status pages
|
||||||
|
- **Notification** - Alert configuration (email, Discord, Slack, webhooks)
|
||||||
|
- **MaintenanceWindow** - Scheduled maintenance periods
|
||||||
|
- **AppSettings** - Global application settings
|
||||||
@@ -79,9 +79,8 @@ You can see the memory footprint of MongoDB and Redis on the same server (398Mb
|
|||||||
|
|
||||||
If you have any questions, suggestions or comments, you have several options:
|
If you have any questions, suggestions or comments, you have several options:
|
||||||
|
|
||||||
- [Discord channel](https://discord.gg/NAb6H3UTjK)
|
- [Discord channel](https://discord.gg/NAb6H3UTjK) (preferred)
|
||||||
- [GitHub Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions)
|
- [GitHub Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions) (we check here from time to time)
|
||||||
- [Reddit group](https://www.reddit.com/r/CheckmateMonitoring/)
|
|
||||||
|
|
||||||
Feel free to ask questions or share your ideas - we'd love to hear from you!
|
Feel free to ask questions or share your ideas - we'd love to hear from you!
|
||||||
|
|
||||||
|
|||||||
Generated
+1150
-1756
File diff suppressed because it is too large.
Load diff
+15
-10
@@ -12,16 +12,18 @@
|
|||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format-check": "prettier --check ."
|
"format-check": "prettier --check ."
|
||||||
},
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"**/*": "npm run format"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.13.3",
|
"@emotion/react": "^11.13.3",
|
||||||
"@emotion/styled": "^11.13.0",
|
"@emotion/styled": "^11.13.0",
|
||||||
"@hello-pangea/dnd": "^18.0.0",
|
"@hello-pangea/dnd": "^18.0.0",
|
||||||
"@hookform/resolvers": "5.2.2",
|
"@hookform/resolvers": "5.2.2",
|
||||||
"@mui/icons-material": "6.4.11",
|
"@mui/lab": "7.0.0",
|
||||||
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
|
"@mui/material": "7.3.7",
|
||||||
"@mui/material": "6.4.11",
|
"@mui/x-charts": "7.29.1",
|
||||||
"@mui/x-charts": "^7.5.1",
|
"@mui/x-date-pickers": "7.29.4",
|
||||||
"@mui/x-date-pickers": "7.29.0",
|
|
||||||
"@reduxjs/toolkit": "2.7.0",
|
"@reduxjs/toolkit": "2.7.0",
|
||||||
"axios": "^1.7.4",
|
"axios": "^1.7.4",
|
||||||
"dayjs": "1.11.13",
|
"dayjs": "1.11.13",
|
||||||
@@ -30,11 +32,13 @@
|
|||||||
"human-interval": "2.0.1",
|
"human-interval": "2.0.1",
|
||||||
"i18next": "25.4.2",
|
"i18next": "25.4.2",
|
||||||
"joi": "17.13.3",
|
"joi": "17.13.3",
|
||||||
"mui-color-input": "^6.0.0",
|
"lucide-react": "^0.562.0",
|
||||||
"pretty-ms": "9.3.0",
|
"mui-color-input": "^7.0.0",
|
||||||
|
"pretty-bytes": "^7.1.0",
|
||||||
|
"pretty-ms": "^9.3.0",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "7.63.0",
|
"react-hook-form": "^7.63.0",
|
||||||
"react-i18next": "^15.4.0",
|
"react-i18next": "^15.4.0",
|
||||||
"react-icons": "5.5.0",
|
"react-icons": "5.5.0",
|
||||||
"react-redux": "9.2.0",
|
"react-redux": "9.2.0",
|
||||||
@@ -43,9 +47,9 @@
|
|||||||
"react-toastify": "^10.0.5",
|
"react-toastify": "^10.0.5",
|
||||||
"recharts": "2.15.2",
|
"recharts": "2.15.2",
|
||||||
"redux-persist": "6.0.0",
|
"redux-persist": "6.0.0",
|
||||||
"swr": "2.3.6",
|
"swr": "^2.3.6",
|
||||||
"vite-plugin-svgr": "^4.2.0",
|
"vite-plugin-svgr": "^4.2.0",
|
||||||
"zod": "4.1.11"
|
"zod": "^4.1.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "24.5.2",
|
"@types/node": "24.5.2",
|
||||||
@@ -56,6 +60,7 @@
|
|||||||
"eslint-plugin-react": "^7.34.1",
|
"eslint-plugin-react": "^7.34.1",
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.6",
|
"eslint-plugin-react-refresh": "^0.4.6",
|
||||||
|
"lint-staged": "^16.2.7",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"vite": "6.3.6"
|
"vite": "6.3.6"
|
||||||
|
|||||||
+18
-2
@@ -22,13 +22,29 @@ function App() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const theme = mode === "light" ? lightTheme : darkTheme;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
|
<GlobalStyles
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
backgroundColor: theme.palette.background.main,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
<Routes />
|
<Routes />
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
<ToastContainer />
|
<ToastContainer
|
||||||
|
newestOnTop={true}
|
||||||
|
theme={mode}
|
||||||
|
style={{
|
||||||
|
"--toastify-color-progress-light": "#7C8BA1",
|
||||||
|
"--toastify-color-progress-dark": "#7C8BA1",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import Menu from "@mui/material/Menu";
|
import Menu from "@mui/material/Menu";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import Settings from "../../../assets/icons/settings-bold.svg?react";
|
import Icon from "../Icon";
|
||||||
import Dialog from "../Dialog/index.jsx";
|
import Dialog from "../Dialog/index.jsx";
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
@@ -32,7 +32,6 @@ const ActionsMenu = ({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
let monitor = { id: actions.id };
|
let monitor = { id: actions.id };
|
||||||
console.log(monitor);
|
|
||||||
await deleteMonitor({ monitor });
|
await deleteMonitor({ monitor });
|
||||||
updateRowCallback();
|
updateRowCallback();
|
||||||
};
|
};
|
||||||
@@ -84,7 +83,10 @@ const ActionsMenu = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Settings />
|
<Icon
|
||||||
|
name="Settings"
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<Menu
|
<Menu
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import { Box, Button, IconButton, Stack, Typography } from "@mui/material";
|
import { Box, Button, IconButton, Stack, Typography } from "@mui/material";
|
||||||
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
import Icon from "../Icon";
|
||||||
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
|
|
||||||
import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined";
|
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -13,9 +10,24 @@ import "./index.css";
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
info: <InfoOutlinedIcon />,
|
info: (
|
||||||
error: <ErrorOutlineOutlinedIcon />,
|
<Icon
|
||||||
warning: <WarningAmberOutlinedIcon />,
|
name="Info"
|
||||||
|
size={24}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
error: (
|
||||||
|
<Icon
|
||||||
|
name="AlertCircle"
|
||||||
|
size={24}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
warning: (
|
||||||
|
<Icon
|
||||||
|
name="AlertTriangle"
|
||||||
|
size={24}
|
||||||
|
/>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -97,10 +109,9 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CloseIcon
|
<Icon
|
||||||
sx={{
|
name="X"
|
||||||
fontSize: "20px",
|
size={20}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,27 +1,28 @@
|
|||||||
import LeftArrow from "../../../assets/icons/left-arrow.svg?react";
|
import Icon from "../Icon";
|
||||||
import LeftArrowDouble from "../../../assets/icons/left-arrow-double.svg?react";
|
|
||||||
import LeftArrowLong from "../../../assets/icons/left-arrow-long.svg?react";
|
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
const ArrowLeft = ({ type, color = "#667085", ...props }) => {
|
const ArrowLeft = ({ type, color = "#667085", ...props }) => {
|
||||||
if (type === "double") {
|
if (type === "double") {
|
||||||
return (
|
return (
|
||||||
<LeftArrowDouble
|
<Icon
|
||||||
style={{ color }}
|
name="ChevronsLeft"
|
||||||
|
color={color}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (type === "long") {
|
} else if (type === "long") {
|
||||||
return (
|
return (
|
||||||
<LeftArrowLong
|
<Icon
|
||||||
style={{ color }}
|
name="ArrowLeft"
|
||||||
|
color={color}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<LeftArrow
|
<Icon
|
||||||
style={{ color }}
|
name="ChevronLeft"
|
||||||
|
color={color}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import RightArrow from "../../../assets/icons/right-arrow.svg?react";
|
import Icon from "../Icon";
|
||||||
import RightArrowDouble from "../../../assets/icons/right-arrow-double.svg?react";
|
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
const ArrowRight = ({ type, color = "#667085", ...props }) => {
|
const ArrowRight = ({ type, color = "#667085", ...props }) => {
|
||||||
if (type === "double") {
|
if (type === "double") {
|
||||||
return (
|
return (
|
||||||
<RightArrowDouble
|
<Icon
|
||||||
style={{ color }}
|
name="ChevronsRight"
|
||||||
|
color={color}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<RightArrow
|
<Icon
|
||||||
style={{ color }}
|
name="ChevronRight"
|
||||||
|
color={color}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import { Box, Stack, Tooltip, Typography } from "@mui/material";
|
import { Box, Stack, Tooltip, Typography } from "@mui/material";
|
||||||
import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
|
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Stack, Box, Tooltip, Typography } from "@mui/material";
|
|||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
|
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
const PlaceholderCheck = ({ daysToShow }) => {
|
const PlaceholderCheck = ({ daysToShow }) => {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { Box, Stack, Typography } from "@mui/material";
|
|||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
|
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
|
||||||
import {
|
import {
|
||||||
tooltipDateFormatLookup,
|
tooltipDateFormatLookup,
|
||||||
tickDateFormatLookup,
|
tickDateFormatLookup,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import { Box, Stack, Tooltip, Typography } from "@mui/material";
|
import { Box, Stack, Tooltip, Typography } from "@mui/material";
|
||||||
import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
|
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
|
||||||
import { useEffect, useState, forwardRef } from "react";
|
import { useEffect, useState, forwardRef } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
@@ -148,7 +148,7 @@ const StatusPageBarChart = ({ checks = [] }) => {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
placement="top"
|
placement="top"
|
||||||
key={`check-${check?._id}`}
|
key={`check-${check?.id}`}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
popper: {
|
popper: {
|
||||||
className: "bar-tooltip",
|
className: "bar-tooltip",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import PropTypes from "prop-types";
|
|||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { useTheme } from "@mui/material";
|
import { useTheme } from "@mui/material";
|
||||||
import { Text } from "recharts";
|
import { Text } from "recharts";
|
||||||
import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
|
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
|
||||||
import { Box, Stack, Typography } from "@mui/material";
|
import { Box, Stack, Typography } from "@mui/material";
|
||||||
import { tickDateFormatLookup, tooltipDateFormatLookup } from "./chartUtilFunctions.js";
|
import { tickDateFormatLookup, tooltipDateFormatLookup } from "./chartUtilFunctions.js";
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import "./check.css";
|
import "./check.css";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import CheckGrey from "../../../assets/icons/check.svg?react";
|
import Icon from "../Icon";
|
||||||
import CheckOutlined from "../../../assets/icons/check-outlined.svg?react";
|
|
||||||
import { Box, Stack, Typography } from "@mui/material";
|
import { Box, Stack, Typography } from "@mui/material";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
|
|
||||||
@@ -35,16 +34,16 @@ const Check = ({ text, noHighlightText, variant = "info", outlined = false }) =>
|
|||||||
alignItems="center"
|
alignItems="center"
|
||||||
>
|
>
|
||||||
{outlined ? (
|
{outlined ? (
|
||||||
<CheckOutlined alt="check" />
|
<Icon
|
||||||
|
name="CheckCircle"
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Box
|
<Icon
|
||||||
lineHeight={0}
|
name="Check"
|
||||||
sx={{
|
size={16}
|
||||||
"& svg > path": { fill: colors[variant] },
|
color={colors[variant]}
|
||||||
}}
|
/>
|
||||||
>
|
|
||||||
<CheckGrey alt="form checks" />
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
<Typography
|
<Typography
|
||||||
component="span"
|
component="span"
|
||||||
@@ -57,7 +56,14 @@ const Check = ({ text, noHighlightText, variant = "info", outlined = false }) =>
|
|||||||
fontWeight: 450,
|
fontWeight: 450,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{noHighlightText && <Typography component="span">{noHighlightText}</Typography>}{" "}
|
{noHighlightText && (
|
||||||
|
<Typography
|
||||||
|
component="span"
|
||||||
|
sx={{ color: "inherit" }}
|
||||||
|
>
|
||||||
|
{noHighlightText}
|
||||||
|
</Typography>
|
||||||
|
)}{" "}
|
||||||
{text}
|
{text}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ import IconButton from "@mui/material/IconButton";
|
|||||||
import Container from "@mui/material/Container";
|
import Container from "@mui/material/Container";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import Drawer from "@mui/material/Drawer";
|
import Drawer from "@mui/material/Drawer";
|
||||||
import MenuIcon from "@mui/icons-material/Menu";
|
import Icon from "../Icon";
|
||||||
import CloseRoundedIcon from "@mui/icons-material/CloseRounded";
|
|
||||||
import ThemeSwitch from "../ThemeSwitch/index.jsx";
|
import ThemeSwitch from "../ThemeSwitch/index.jsx";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
@@ -153,7 +152,11 @@ const AppAppBar = () => {
|
|||||||
aria-label="Menu button"
|
aria-label="Menu button"
|
||||||
onClick={toggleDrawer(true)}
|
onClick={toggleDrawer(true)}
|
||||||
>
|
>
|
||||||
<MenuIcon sx={{ color: theme.palette.text.primary }} />
|
<Icon
|
||||||
|
name="Menu"
|
||||||
|
size={24}
|
||||||
|
color={theme.palette.text.primary}
|
||||||
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Drawer
|
<Drawer
|
||||||
anchor="top"
|
anchor="top"
|
||||||
@@ -176,7 +179,11 @@ const AppAppBar = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconButton onClick={toggleDrawer(false)}>
|
<IconButton onClick={toggleDrawer(false)}>
|
||||||
<CloseRoundedIcon sx={{ color: theme.palette.text.primary }} />
|
<Icon
|
||||||
|
name="X"
|
||||||
|
size={24}
|
||||||
|
color={theme.palette.text.primary}
|
||||||
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
import { Checkbox, FormControl, ListItemText, MenuItem, Select } from "@mui/material";
|
|
||||||
import { useTheme } from "@emotion/react";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A reusable filter header component that displays a dropdown menu with selectable options.
|
|
||||||
*
|
|
||||||
* @component
|
|
||||||
* @param {Object} props - The component props.
|
|
||||||
* @param {string} props.header - The header text to display when no options are selected.
|
|
||||||
* @param {Array} props.options - An array of options to display in the dropdown menu. Each option should have a `value` and `label`.
|
|
||||||
* @param {Array} [props.value] - The currently selected values.
|
|
||||||
* @param {Function} props.onChange - The callback function to handle changes in the selected values.
|
|
||||||
* @param {boolean} [props.multiple=true] - Whether multiple options can be selected.
|
|
||||||
* @returns {JSX.Element} The rendered FilterHeader component.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const FilterHeader = ({ header, options, value, onChange, multiple = true }) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const controlledValue = value === undefined ? [] : value; // Ensure value is always treated as an array for controlled component purposes
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
sx={{ minWidth: "10%" }}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
multiple={multiple}
|
|
||||||
IconComponent={(props) => (
|
|
||||||
<AddCircleOutlineIcon
|
|
||||||
{...props}
|
|
||||||
sx={{ fontSize: "medium" }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
displayEmpty
|
|
||||||
value={controlledValue}
|
|
||||||
onChange={onChange}
|
|
||||||
renderValue={(selected) => {
|
|
||||||
if (!selected?.length) {
|
|
||||||
return header;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
header +
|
|
||||||
" | " +
|
|
||||||
selected
|
|
||||||
.map((value) => options.find((option) => option.value === value)?.label)
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(", ")
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
MenuProps={{
|
|
||||||
anchorOrigin: {
|
|
||||||
vertical: "bottom",
|
|
||||||
horizontal: "left",
|
|
||||||
},
|
|
||||||
transformOrigin: {
|
|
||||||
vertical: "top",
|
|
||||||
horizontal: "left",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{options.map((option) => (
|
|
||||||
<MenuItem
|
|
||||||
key={option.value}
|
|
||||||
value={option.value}
|
|
||||||
sx={{
|
|
||||||
height: theme.spacing(17),
|
|
||||||
padding: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={controlledValue.includes(option.value)}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
<ListItemText primary={option.label} />
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
FilterHeader.propTypes = {
|
|
||||||
header: PropTypes.string.isRequired,
|
|
||||||
options: PropTypes.arrayOf(
|
|
||||||
PropTypes.shape({
|
|
||||||
value: PropTypes.string.isRequired,
|
|
||||||
label: PropTypes.string.isRequired,
|
|
||||||
})
|
|
||||||
).isRequired,
|
|
||||||
value: PropTypes.arrayOf(PropTypes.string),
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
multiple: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FilterHeader;
|
|
||||||
@@ -23,12 +23,12 @@ const Host = ({ url, title, percentageColor, percentage, showURL, status }) => {
|
|||||||
direction="row"
|
direction="row"
|
||||||
position="relative"
|
position="relative"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
gap={theme.spacing(5)}
|
gap={theme.spacing(4)}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
variant="h6"
|
component="span"
|
||||||
sx={{
|
sx={{
|
||||||
fontWeight: 600,
|
fontWeight: 500,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
@@ -38,9 +38,9 @@ const Host = ({ url, title, percentageColor, percentage, showURL, status }) => {
|
|||||||
<>
|
<>
|
||||||
<Dot />
|
<Dot />
|
||||||
<Typography
|
<Typography
|
||||||
variant="h6"
|
component="span"
|
||||||
sx={{
|
sx={{
|
||||||
fontWeight: 600,
|
fontWeight: 500,
|
||||||
color: percentageColor,
|
color: percentageColor,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* Maps legacy icon names to Lucide icon names
|
||||||
|
* Use: import { iconMap } from './iconMap'; LucideIcons[iconMap['monitors']]
|
||||||
|
*/
|
||||||
|
export const iconMap = {
|
||||||
|
// Navigation/Sidebar icons
|
||||||
|
monitors: "Globe",
|
||||||
|
incidents: "AlertTriangle",
|
||||||
|
integrations: "Link",
|
||||||
|
"page-speed": "Gauge",
|
||||||
|
settings: "Settings",
|
||||||
|
notifications: "Bell",
|
||||||
|
maintenance: "Wrench",
|
||||||
|
"status-pages": "Wifi",
|
||||||
|
docs: "FileText",
|
||||||
|
discussions: "MessageCircle",
|
||||||
|
support: "HelpCircle",
|
||||||
|
changeLog: "Code",
|
||||||
|
logs: "Database",
|
||||||
|
|
||||||
|
// User icons
|
||||||
|
user: "User",
|
||||||
|
"user-two": "Users",
|
||||||
|
"user-edit": "UserPen",
|
||||||
|
groups: "Users",
|
||||||
|
lock: "Lock",
|
||||||
|
key: "Key",
|
||||||
|
logout: "LogOut",
|
||||||
|
|
||||||
|
// Action icons
|
||||||
|
edit: "Pencil",
|
||||||
|
"trash-bin": "Trash2",
|
||||||
|
search: "Search",
|
||||||
|
"close-icon": "X",
|
||||||
|
check: "Check",
|
||||||
|
"check-icon": "CheckCircle",
|
||||||
|
"check-outlined": "CheckCircle",
|
||||||
|
|
||||||
|
// Arrow icons
|
||||||
|
"left-arrow": "ChevronLeft",
|
||||||
|
"right-arrow": "ChevronRight",
|
||||||
|
"up-arrow": "ChevronUp",
|
||||||
|
"down-arrow": "ChevronDown",
|
||||||
|
"left-arrow-double": "ChevronsLeft",
|
||||||
|
"right-arrow-double": "ChevronsRight",
|
||||||
|
"left-arrow-long": "ArrowLeft",
|
||||||
|
"top-right-arrow": "ArrowUpRight",
|
||||||
|
"open-in-new-page": "ExternalLink",
|
||||||
|
"selector-vertical": "ChevronsUpDown",
|
||||||
|
|
||||||
|
// Status/Alert icons
|
||||||
|
"alert-icon": "Bell",
|
||||||
|
"warning-icon": "AlertCircle",
|
||||||
|
"history-icon": "History",
|
||||||
|
|
||||||
|
// Data/Chart icons
|
||||||
|
dashboard: "LayoutGrid",
|
||||||
|
"monitor-graph-line": "TrendingUp",
|
||||||
|
"response-time-icon": "TrendingUp",
|
||||||
|
"uptime-icon": "Activity",
|
||||||
|
"speedometer-icon": "Gauge",
|
||||||
|
"spedometer-icon": "Gauge",
|
||||||
|
"ruler-icon": "Ruler",
|
||||||
|
"performance-report": "Layers",
|
||||||
|
"average-response-icon": "BarChart3",
|
||||||
|
"distributed-uptime": "Network",
|
||||||
|
|
||||||
|
// Misc icons
|
||||||
|
calendar: "Calendar",
|
||||||
|
"calendar-check": "CalendarCheck",
|
||||||
|
"clock-snooze": "Clock",
|
||||||
|
mail: "Mail",
|
||||||
|
email: "Mail",
|
||||||
|
folder: "Folder",
|
||||||
|
"dots-vertical": "MoreVertical",
|
||||||
|
"pause-icon": "PauseCircle",
|
||||||
|
"resume-icon": "PlayCircle",
|
||||||
|
"checkbox-filled": "CheckSquare",
|
||||||
|
"checkbox-outline": "Square",
|
||||||
|
"radio-checked": "Circle",
|
||||||
|
"cpu-chip": "Cpu",
|
||||||
|
certificate: "Award",
|
||||||
|
"interval-check": "RefreshCw",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MUI icon to Lucide mapping
|
||||||
|
*/
|
||||||
|
export const muiToLucideMap = {
|
||||||
|
AddCircleOutline: "PlusCircle",
|
||||||
|
ArrowDownwardRounded: "ArrowDown",
|
||||||
|
ArrowUpwardRounded: "ArrowUp",
|
||||||
|
ArrowDropDown: "ChevronDown",
|
||||||
|
ArrowOutward: "ExternalLink",
|
||||||
|
CheckCircle: "CheckCircle2",
|
||||||
|
Clear: "X",
|
||||||
|
Close: "X",
|
||||||
|
CloseRounded: "X",
|
||||||
|
CloudUpload: "Upload",
|
||||||
|
Delete: "Trash2",
|
||||||
|
DeleteOutlineRounded: "Trash2",
|
||||||
|
Email: "Mail",
|
||||||
|
ErrorOutline: "AlertTriangle",
|
||||||
|
ErrorOutlineOutlined: "AlertTriangle",
|
||||||
|
Image: "ImageIcon",
|
||||||
|
InfoOutlined: "Info",
|
||||||
|
KeyboardArrowDown: "ChevronDown",
|
||||||
|
Menu: "Menu",
|
||||||
|
PauseCircleOutline: "PauseCircle",
|
||||||
|
PauseOutlined: "Pause",
|
||||||
|
PlayArrowOutlined: "Play",
|
||||||
|
PlayCircleOutlineRounded: "PlayCircle",
|
||||||
|
RadioButtonChecked: "Circle",
|
||||||
|
ReorderRounded: "GripVertical",
|
||||||
|
SettingsOutlined: "Settings",
|
||||||
|
Share: "Share2",
|
||||||
|
Visibility: "Eye",
|
||||||
|
VisibilityOff: "EyeOff",
|
||||||
|
WarningAmberOutlined: "AlertTriangle",
|
||||||
|
};
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
import { useTheme } from "@mui/material";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import {
|
||||||
|
Activity,
|
||||||
|
AlertCircle,
|
||||||
|
AlertTriangle,
|
||||||
|
ArrowDown,
|
||||||
|
ArrowLeft,
|
||||||
|
ArrowUp,
|
||||||
|
ArrowUpRight,
|
||||||
|
Bell,
|
||||||
|
Calendar,
|
||||||
|
Check,
|
||||||
|
CheckCircle,
|
||||||
|
CheckCircle2,
|
||||||
|
CheckSquare,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
ChevronsLeft,
|
||||||
|
ChevronsRight,
|
||||||
|
ChevronsUpDown,
|
||||||
|
Circle,
|
||||||
|
CircleDot,
|
||||||
|
Clock,
|
||||||
|
Code,
|
||||||
|
Cpu,
|
||||||
|
Database,
|
||||||
|
ExternalLink,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
FileText,
|
||||||
|
Gauge,
|
||||||
|
Globe,
|
||||||
|
GripVertical,
|
||||||
|
HelpCircle,
|
||||||
|
History,
|
||||||
|
Image as ImageIcon,
|
||||||
|
Info,
|
||||||
|
Key,
|
||||||
|
Layers,
|
||||||
|
Link,
|
||||||
|
Lock,
|
||||||
|
LogOut,
|
||||||
|
Mail,
|
||||||
|
Menu,
|
||||||
|
MessageCircle,
|
||||||
|
MoreVertical,
|
||||||
|
Pause,
|
||||||
|
PauseCircle,
|
||||||
|
Play,
|
||||||
|
PlayCircle,
|
||||||
|
PlusCircle,
|
||||||
|
RefreshCw,
|
||||||
|
Ruler,
|
||||||
|
Search,
|
||||||
|
Settings,
|
||||||
|
Square,
|
||||||
|
Trash2,
|
||||||
|
TrendingUp,
|
||||||
|
Upload,
|
||||||
|
User,
|
||||||
|
Users,
|
||||||
|
Wifi,
|
||||||
|
Wrench,
|
||||||
|
X,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of icon names to Lucide icon components.
|
||||||
|
* Only icons explicitly imported here will be included in the bundle (tree-shaking).
|
||||||
|
* To add a new icon: import it above and add it to this object.
|
||||||
|
*/
|
||||||
|
const iconComponents = {
|
||||||
|
Activity,
|
||||||
|
AlertCircle,
|
||||||
|
AlertTriangle,
|
||||||
|
ArrowDown,
|
||||||
|
ArrowLeft,
|
||||||
|
ArrowUp,
|
||||||
|
ArrowUpRight,
|
||||||
|
Bell,
|
||||||
|
Calendar,
|
||||||
|
Check,
|
||||||
|
CheckCircle,
|
||||||
|
CheckCircle2,
|
||||||
|
CheckSquare,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
ChevronsLeft,
|
||||||
|
ChevronsRight,
|
||||||
|
ChevronsUpDown,
|
||||||
|
Circle,
|
||||||
|
CircleDot,
|
||||||
|
Clock,
|
||||||
|
Code,
|
||||||
|
Cpu,
|
||||||
|
Database,
|
||||||
|
ExternalLink,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
FileText,
|
||||||
|
Gauge,
|
||||||
|
Globe,
|
||||||
|
GripVertical,
|
||||||
|
HelpCircle,
|
||||||
|
History,
|
||||||
|
Image: ImageIcon,
|
||||||
|
Info,
|
||||||
|
Key,
|
||||||
|
Layers,
|
||||||
|
Link,
|
||||||
|
Lock,
|
||||||
|
LogOut,
|
||||||
|
Mail,
|
||||||
|
Menu,
|
||||||
|
MessageCircle,
|
||||||
|
MoreVertical,
|
||||||
|
Pause,
|
||||||
|
PauseCircle,
|
||||||
|
Play,
|
||||||
|
PlayCircle,
|
||||||
|
PlusCircle,
|
||||||
|
RefreshCw,
|
||||||
|
Ruler,
|
||||||
|
Search,
|
||||||
|
Settings,
|
||||||
|
Square,
|
||||||
|
Trash2,
|
||||||
|
TrendingUp,
|
||||||
|
Upload,
|
||||||
|
User,
|
||||||
|
Users,
|
||||||
|
Wifi,
|
||||||
|
Wrench,
|
||||||
|
X,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Theme-aware icon component wrapping Lucide icons
|
||||||
|
*
|
||||||
|
* @param {string} name - Lucide icon name (e.g., "Bell", "Check", "ArrowLeft")
|
||||||
|
* @param {number} size - Icon size in pixels (default: 20)
|
||||||
|
* @param {string} color - Direct color value OR theme path like "primary.contrastText"
|
||||||
|
* @param {number} strokeWidth - Stroke width (default: 1.5)
|
||||||
|
* @param {string} fill - Fill color (default: "none")
|
||||||
|
*/
|
||||||
|
const Icon = ({ name, size = 20, color, strokeWidth = 1.5, fill = "none", ...props }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
// Resolve color from theme path or use direct value
|
||||||
|
const resolveColor = (colorValue) => {
|
||||||
|
if (!colorValue) {
|
||||||
|
return theme.palette.primary.contrastTextTertiary; // Default icon color
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a theme path like "primary.contrastText"
|
||||||
|
if (typeof colorValue === "string" && colorValue.includes(".")) {
|
||||||
|
const parts = colorValue.split(".");
|
||||||
|
let resolved = theme.palette;
|
||||||
|
for (const part of parts) {
|
||||||
|
resolved = resolved?.[part];
|
||||||
|
}
|
||||||
|
return resolved || colorValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct color value
|
||||||
|
return colorValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LucideIcon = iconComponents[name];
|
||||||
|
|
||||||
|
if (!LucideIcon) {
|
||||||
|
console.warn(`Icon "${name}" not found in Icon component`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LucideIcon
|
||||||
|
size={size}
|
||||||
|
color={resolveColor(color)}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
fill={fill}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Icon.propTypes = {
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
size: PropTypes.number,
|
||||||
|
color: PropTypes.string,
|
||||||
|
strokeWidth: PropTypes.number,
|
||||||
|
fill: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Icon;
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { FormControlLabel, Checkbox as MuiCheckbox } from "@mui/material";
|
import { FormControlLabel, Checkbox as MuiCheckbox } from "@mui/material";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import CheckboxOutline from "../../../../assets/icons/checkbox-outline.svg?react";
|
import Icon from "../../Icon";
|
||||||
import CheckboxFilled from "../../../../assets/icons/checkbox-filled.svg?react";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checkbox Component
|
* Checkbox Component
|
||||||
@@ -65,8 +64,8 @@ const Checkbox = ({
|
|||||||
name={name}
|
name={name}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
icon={<CheckboxOutline />}
|
icon={<Icon name="Square" />}
|
||||||
checkedIcon={<CheckboxFilled />}
|
checkedIcon={<Icon name="CheckSquare" />}
|
||||||
inputProps={{
|
inputProps={{
|
||||||
"aria-label": "controlled checkbox",
|
"aria-label": "controlled checkbox",
|
||||||
id: id,
|
id: id,
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
// Components
|
// Components
|
||||||
import { Box, Stack, Typography } from "@mui/material";
|
import { Box, Stack, Typography } from "@mui/material";
|
||||||
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
|
import Icon from "../../Icon";
|
||||||
import Image from "../../Image/index.jsx";
|
import Image from "../../Image/index.jsx";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import ProgressUpload from "../../ProgressBars/index.jsx";
|
import ProgressUpload from "../../ProgressBars/index.jsx";
|
||||||
import ImageIcon from "@mui/icons-material/Image";
|
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
@@ -176,7 +175,10 @@ const ImageUpload = ({
|
|||||||
boxShadow: theme.shape.boxShadow,
|
boxShadow: theme.shape.boxShadow,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CloudUploadIcon />
|
<Icon
|
||||||
|
name="Upload"
|
||||||
|
size={24}
|
||||||
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography
|
<Typography
|
||||||
component="h2"
|
component="h2"
|
||||||
@@ -203,7 +205,12 @@ const ImageUpload = ({
|
|||||||
</Box>
|
</Box>
|
||||||
{(localError || progress.isLoading || progress.value !== 0) && (
|
{(localError || progress.isLoading || progress.value !== 0) && (
|
||||||
<ProgressUpload
|
<ProgressUpload
|
||||||
icon={<ImageIcon />}
|
icon={
|
||||||
|
<Icon
|
||||||
|
name="Image"
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={file?.name || "Upload failed"}
|
label={file?.name || "Upload failed"}
|
||||||
size={file?.size}
|
size={file?.size}
|
||||||
progress={progress.value}
|
progress={progress.value}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { FormControlLabel, Radio as MUIRadio, Typography } from "@mui/material";
|
import { FormControlLabel, Radio as MUIRadio, Typography } from "@mui/material";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import RadioChecked from "../../../../assets/icons/radio-checked.svg?react";
|
import Icon from "../../Icon";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,7 +47,12 @@ const Radio = ({
|
|||||||
<MUIRadio
|
<MUIRadio
|
||||||
id={id}
|
id={id}
|
||||||
size={size}
|
size={size}
|
||||||
checkedIcon={<RadioChecked />}
|
checkedIcon={
|
||||||
|
<Icon
|
||||||
|
name="CircleDot"
|
||||||
|
size={16}
|
||||||
|
/>
|
||||||
|
}
|
||||||
sx={{
|
sx={{
|
||||||
color: "transparent",
|
color: "transparent",
|
||||||
width: 16,
|
width: 16,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
Checkbox,
|
Checkbox,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import SearchIcon from "../../../../assets/icons/search.svg?react";
|
import Icon from "../../Icon";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import FieldWrapper from "../FieldWrapper/index.jsx";
|
import FieldWrapper from "../FieldWrapper/index.jsx";
|
||||||
@@ -31,20 +31,13 @@ import FieldWrapper from "../FieldWrapper/index.jsx";
|
|||||||
const SearchAdornment = () => {
|
const SearchAdornment = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box height={16}>
|
||||||
height={16}
|
<Icon
|
||||||
sx={{
|
name="Search"
|
||||||
"& svg": {
|
size={16}
|
||||||
width: 16,
|
color="primary.contrastTextTertiary"
|
||||||
height: 16,
|
strokeWidth={1.2}
|
||||||
"& path": {
|
/>
|
||||||
stroke: theme.palette.primary.contrastTextTertiary,
|
|
||||||
strokeWidth: 1.2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SearchIcon />
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import { MenuItem, Select as MuiSelect, Stack, Typography } from "@mui/material";
|
import { MenuItem, Select as MuiSelect, Stack, Typography } from "@mui/material";
|
||||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
import Icon from "../../Icon";
|
||||||
import FieldWrapper from "../FieldWrapper/index.jsx";
|
import FieldWrapper from "../FieldWrapper/index.jsx";
|
||||||
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
@@ -98,7 +98,13 @@ const Select = ({
|
|||||||
error={error}
|
error={error}
|
||||||
name={name}
|
name={name}
|
||||||
inputProps={{ id: id }}
|
inputProps={{ id: id }}
|
||||||
IconComponent={KeyboardArrowDownIcon}
|
IconComponent={(props) => (
|
||||||
|
<Icon
|
||||||
|
name="ChevronDown"
|
||||||
|
size={20}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
MenuProps={{ disableScrollLock: true }}
|
MenuProps={{ disableScrollLock: true }}
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { Stack, Typography, InputAdornment, IconButton } from "@mui/material";
|
import { Stack, Typography, InputAdornment, IconButton } from "@mui/material";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import VisibilityOff from "@mui/icons-material/VisibilityOff";
|
import Icon from "../../../Icon";
|
||||||
import Visibility from "@mui/icons-material/Visibility";
|
|
||||||
import ReorderRoundedIcon from "@mui/icons-material/ReorderRounded";
|
|
||||||
import DeleteIcon from "../../../../../assets/icons/trash-bin.svg?react";
|
|
||||||
|
|
||||||
export const HttpAdornment = ({ https }) => {
|
export const HttpAdornment = ({ https }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@@ -56,7 +53,17 @@ export const PasswordEndAdornment = ({ fieldType, setFieldType }) => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{fieldType === "password" ? <VisibilityOff /> : <Visibility />}
|
{fieldType === "password" ? (
|
||||||
|
<Icon
|
||||||
|
name="EyeOff"
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Icon
|
||||||
|
name="Eye"
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO go for this approach for responsiveness. The aside needs to be taken care of */
|
/* TODO go for this approach for responsiveness. The aside needs to be taken care of */
|
||||||
@@ -12,15 +14,7 @@
|
|||||||
}
|
}
|
||||||
} */
|
} */
|
||||||
|
|
||||||
/* .home-layout aside {
|
.home-layout > .home-content-wrapper {
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
height: 100vh;
|
|
||||||
max-width: var(--env-var-side-bar-width);
|
|
||||||
} */
|
|
||||||
|
|
||||||
.home-layout > div {
|
|
||||||
min-height: calc(100vh - var(--env-var-spacing-2) * 2);
|
min-height: calc(100vh - var(--env-var-spacing-2) * 2);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
@@ -30,4 +24,6 @@
|
|||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,27 @@
|
|||||||
import Sidebar from "../../Sidebar/index.jsx";
|
import Sidebar from "../../Sidebar/index.jsx";
|
||||||
import { Outlet } from "react-router";
|
import { Outlet } from "react-router";
|
||||||
import { Stack } from "@mui/material";
|
import { Box, Stack } from "@mui/material";
|
||||||
|
import { useSidebar } from "@/Hooks/useSidebar.js";
|
||||||
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
const HomeLayout = () => {
|
const HomeLayout = () => {
|
||||||
|
const { width, transition } = useSidebar();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
className="home-layout"
|
className="home-layout"
|
||||||
flexDirection="row"
|
flexDirection="row"
|
||||||
gap={14}
|
|
||||||
>
|
>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
{/* Spacer for fixed sidebar */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width,
|
||||||
|
flexShrink: 0,
|
||||||
|
transition,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Stack className="home-content-wrapper">
|
<Stack className="home-content-wrapper">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import Button from "@mui/material/Button";
|
|
||||||
import ButtonGroup from "@mui/material/ButtonGroup";
|
|
||||||
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
|
|
||||||
import ClickAwayListener from "@mui/material/ClickAwayListener";
|
|
||||||
import Grow from "@mui/material/Grow";
|
|
||||||
import Paper from "@mui/material/Paper";
|
|
||||||
import Popper from "@mui/material/Popper";
|
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
|
||||||
import MenuList from "@mui/material/MenuList";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { createToast } from "../../../Utils/toastUtils.jsx";
|
|
||||||
import { useExportMonitors } from "../../../Hooks/monitorHooks.js";
|
|
||||||
|
|
||||||
const MonitorActions = ({ isLoading }) => {
|
|
||||||
const [open, setOpen] = React.useState(false);
|
|
||||||
const anchorRef = React.useRef(null);
|
|
||||||
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [exportMonitors, isExporting] = useExportMonitors();
|
|
||||||
|
|
||||||
const options = [t("monitorActions.import"), t("monitorActions.export")];
|
|
||||||
|
|
||||||
const handleClick = async () => {
|
|
||||||
if (selectedIndex === 0) {
|
|
||||||
// Import
|
|
||||||
navigate("/uptime/bulk-import");
|
|
||||||
} else {
|
|
||||||
// Export
|
|
||||||
const [success, error] = await exportMonitors();
|
|
||||||
if (!success) {
|
|
||||||
createToast({ body: error || t("export.failed") });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMenuItemClick = (event, index) => {
|
|
||||||
setSelectedIndex(index);
|
|
||||||
setOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggle = () => {
|
|
||||||
setOpen((prevOpen) => !prevOpen);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = (event) => {
|
|
||||||
if (anchorRef.current && anchorRef.current.contains(event.target)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<ButtonGroup
|
|
||||||
variant="contained"
|
|
||||||
color="accent"
|
|
||||||
ref={anchorRef}
|
|
||||||
aria-label="Monitor actions"
|
|
||||||
disabled={isLoading || isExporting}
|
|
||||||
>
|
|
||||||
<Button onClick={handleClick}>{options[selectedIndex]}</Button>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
aria-controls={open ? "split-button-menu" : undefined}
|
|
||||||
aria-expanded={open ? "true" : undefined}
|
|
||||||
aria-label="select monitor action"
|
|
||||||
aria-haspopup="menu"
|
|
||||||
onClick={handleToggle}
|
|
||||||
>
|
|
||||||
<ArrowDropDownIcon />
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
<Popper
|
|
||||||
sx={{ zIndex: 1 }}
|
|
||||||
open={open}
|
|
||||||
anchorEl={anchorRef.current}
|
|
||||||
role={undefined}
|
|
||||||
transition
|
|
||||||
disablePortal
|
|
||||||
>
|
|
||||||
{({ TransitionProps, placement }) => (
|
|
||||||
<Grow
|
|
||||||
{...TransitionProps}
|
|
||||||
style={{
|
|
||||||
transformOrigin: placement === "bottom" ? "center top" : "center bottom",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Paper>
|
|
||||||
<ClickAwayListener onClickAway={handleClose}>
|
|
||||||
<MenuList
|
|
||||||
id="split-button-menu"
|
|
||||||
autoFocusItem
|
|
||||||
>
|
|
||||||
{options.map((option, index) => (
|
|
||||||
<MenuItem
|
|
||||||
key={option}
|
|
||||||
selected={index === selectedIndex}
|
|
||||||
onClick={(event) => handleMenuItemClick(event, index)}
|
|
||||||
>
|
|
||||||
{option}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</MenuList>
|
|
||||||
</ClickAwayListener>
|
|
||||||
</Paper>
|
|
||||||
</Grow>
|
|
||||||
)}
|
|
||||||
</Popper>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MonitorActions;
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { Stack, Typography } from "@mui/material";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { useTheme } from "@emotion/react";
|
|
||||||
import SkeletonLayout from "./skeleton.jsx";
|
|
||||||
|
|
||||||
const MonitorCountHeader = ({
|
|
||||||
isLoading = false,
|
|
||||||
monitorCount,
|
|
||||||
heading = "monitors",
|
|
||||||
sx,
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
if (isLoading) return <SkeletonLayout />;
|
|
||||||
|
|
||||||
if (monitorCount === 1) {
|
|
||||||
heading = "monitor";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
alignItems="center"
|
|
||||||
display="flex"
|
|
||||||
width="fit-content"
|
|
||||||
height={theme.spacing(18)}
|
|
||||||
gap={theme.spacing(2)}
|
|
||||||
mt={theme.spacing(2)}
|
|
||||||
px={theme.spacing(4)}
|
|
||||||
pt={theme.spacing(2)}
|
|
||||||
pb={theme.spacing(3)}
|
|
||||||
borderRadius={theme.spacing(1)}
|
|
||||||
sx={{
|
|
||||||
...sx,
|
|
||||||
backgroundColor: theme.palette.secondary.main,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{monitorCount} <Typography component="h2">{heading}</Typography>
|
|
||||||
{children}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
MonitorCountHeader.propTypes = {
|
|
||||||
isLoading: PropTypes.bool,
|
|
||||||
monitorCount: PropTypes.number,
|
|
||||||
heading: PropTypes.string,
|
|
||||||
children: PropTypes.node,
|
|
||||||
sx: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MonitorCountHeader;
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { Stack, Skeleton } from "@mui/material";
|
|
||||||
import { useTheme } from "@emotion/react";
|
|
||||||
|
|
||||||
const SkeletonLayout = () => {
|
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
alignItems="center"
|
|
||||||
gap={theme.spacing(2)}
|
|
||||||
>
|
|
||||||
<Skeleton
|
|
||||||
variant="text"
|
|
||||||
width={100}
|
|
||||||
height={32}
|
|
||||||
/>
|
|
||||||
<Skeleton
|
|
||||||
variant="circular"
|
|
||||||
width={40}
|
|
||||||
height={40}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SkeletonLayout;
|
|
||||||
@@ -3,7 +3,6 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import MonitorActions from "../MonitorActions/index.jsx";
|
|
||||||
|
|
||||||
const CreateMonitorHeader = ({ isAdmin, label, isLoading = true, path, bulkPath }) => {
|
const CreateMonitorHeader = ({ isAdmin, label, isLoading = true, path, bulkPath }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|||||||
@@ -1,141 +0,0 @@
|
|||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import Status from "./status.jsx";
|
|
||||||
import Skeleton from "./skeleton.jsx";
|
|
||||||
import Button from "@mui/material/Button";
|
|
||||||
import { Tooltip } from "@mui/material";
|
|
||||||
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
|
|
||||||
import PauseOutlinedIcon from "@mui/icons-material/PauseOutlined";
|
|
||||||
import PlayArrowOutlinedIcon from "@mui/icons-material/PlayArrowOutlined";
|
|
||||||
import EmailIcon from "@mui/icons-material/Email";
|
|
||||||
|
|
||||||
// Utils
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import { usePauseMonitor } from "../../../Hooks/monitorHooks.js";
|
|
||||||
import { useSendTestEmail } from "../../../Hooks/useSendTestEmail.js";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useTestAllNotifications } from "../../../Hooks/useNotifications.js";
|
|
||||||
/**
|
|
||||||
* MonitorDetailsControlHeader component displays the control header for monitor details.
|
|
||||||
* It includes status display, pause/resume button, and a configure button for admins.
|
|
||||||
*
|
|
||||||
* @component
|
|
||||||
* @param {Object} props - Component props
|
|
||||||
* @param {string} props.path - The base path for navigation
|
|
||||||
* @param {boolean} [props.isLoading=false] - Flag indicating if the data is loading
|
|
||||||
* @param {boolean} [props.isAdmin=false] - Flag indicating if the user is an admin
|
|
||||||
* @param {Object} props.monitor - The monitor object containing details
|
|
||||||
* @param {Function} props.triggerUpdate - Function to trigger an update
|
|
||||||
* @returns {JSX.Element} The rendered component
|
|
||||||
*/
|
|
||||||
const MonitorDetailsControlHeader = ({
|
|
||||||
path,
|
|
||||||
isLoading = false,
|
|
||||||
isAdmin = false,
|
|
||||||
monitor,
|
|
||||||
triggerUpdate,
|
|
||||||
}) => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const theme = useTheme();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [pauseMonitor, isPausing, error] = usePauseMonitor();
|
|
||||||
|
|
||||||
const isTestNotificationsDisabled = monitor?.notifications?.length === 0;
|
|
||||||
|
|
||||||
const tooltipTitle = isTestNotificationsDisabled ? t("testNotificationsDisabled") : "";
|
|
||||||
|
|
||||||
// const [isSending, emailError, sendTestEmail] = useSendTestEmail();
|
|
||||||
|
|
||||||
const [testAllNotifications, isSending, errorAllNotifications] =
|
|
||||||
useTestAllNotifications();
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <Skeleton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
justifyContent="space-between"
|
|
||||||
>
|
|
||||||
<Status monitor={monitor} />
|
|
||||||
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
gap={theme.spacing(2)}
|
|
||||||
>
|
|
||||||
<Tooltip
|
|
||||||
key={monitor?.id}
|
|
||||||
placement="bottom"
|
|
||||||
title={tooltipTitle}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
loading={isSending}
|
|
||||||
startIcon={<EmailIcon />}
|
|
||||||
disabled={isTestNotificationsDisabled}
|
|
||||||
onClick={() => {
|
|
||||||
testAllNotifications({ monitorId: monitor?.id });
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("sendTestNotifications")}
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
onClick={(e) => {
|
|
||||||
navigate(`/incidents/${monitor?.id}`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("menu.incidents")}
|
|
||||||
</Button>
|
|
||||||
{isAdmin && (
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
loading={isPausing}
|
|
||||||
startIcon={
|
|
||||||
monitor?.isActive ? <PauseOutlinedIcon /> : <PlayArrowOutlinedIcon />
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
pauseMonitor({
|
|
||||||
monitorId: monitor?.id,
|
|
||||||
triggerUpdate,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{monitor?.isActive ? "Pause" : "Resume"}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{isAdmin && (
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
startIcon={<SettingsOutlinedIcon />}
|
|
||||||
onClick={() => navigate(`/${path}/configure/${monitor.id}`)}
|
|
||||||
>
|
|
||||||
Configure
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
MonitorDetailsControlHeader.propTypes = {
|
|
||||||
path: PropTypes.string,
|
|
||||||
isLoading: PropTypes.bool,
|
|
||||||
isAdmin: PropTypes.bool,
|
|
||||||
monitor: PropTypes.object,
|
|
||||||
triggerUpdate: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MonitorDetailsControlHeader;
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { Stack, Skeleton } from "@mui/material";
|
|
||||||
|
|
||||||
const SkeletonLayout = () => {
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
justifyContent="space-between"
|
|
||||||
>
|
|
||||||
<Skeleton
|
|
||||||
height={40}
|
|
||||||
variant="rounded"
|
|
||||||
width="15%"
|
|
||||||
/>
|
|
||||||
<Skeleton
|
|
||||||
height={40}
|
|
||||||
variant="rounded"
|
|
||||||
width="15%"
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SkeletonLayout;
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
// Components
|
|
||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import PulseDot from "../Animated/PulseDot.jsx";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import Dot from "../Dot/index.jsx";
|
|
||||||
// Utils
|
|
||||||
import { formatDurationRounded } from "../../../Utils/timeUtils.js";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { useTheme } from "@emotion/react";
|
|
||||||
import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js";
|
|
||||||
import { formatMonitorUrl } from "../../../Utils/utils.js";
|
|
||||||
/**
|
|
||||||
* Status component displays the status information of a monitor.
|
|
||||||
* It includes the monitor's name, URL, and check interval.
|
|
||||||
*
|
|
||||||
* @component
|
|
||||||
* @param {Object} props - Component props
|
|
||||||
* @param {Object} props.monitor - The monitor object containing details
|
|
||||||
* @param {string} props.monitor.name - The name of the monitor
|
|
||||||
* @param {string} props.monitor.url - The URL of the monitor
|
|
||||||
* @param {number} props.monitor.interval - The interval at which the monitor checks
|
|
||||||
* @returns {JSX.Element} The rendered component
|
|
||||||
*/
|
|
||||||
const Status = ({ monitor }) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const { statusColor, determineState } = useMonitorUtils();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
<Typography variant="monitorName">{monitor?.name}</Typography>
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
alignItems={"center"}
|
|
||||||
gap={theme.spacing(4)}
|
|
||||||
>
|
|
||||||
<PulseDot color={statusColor[determineState(monitor)]} />
|
|
||||||
<Typography variant="monitorUrl">{formatMonitorUrl(monitor?.url)}</Typography>
|
|
||||||
<Dot />
|
|
||||||
<Typography>
|
|
||||||
Checking every {formatDurationRounded(monitor?.interval)}.
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Status.propTypes = {
|
|
||||||
monitor: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Status;
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
// Components
|
|
||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import Divider from "@mui/material/Divider";
|
|
||||||
import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
|
|
||||||
import Search from "../Inputs/Search/index.jsx";
|
|
||||||
|
|
||||||
// Utils
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
|
|
||||||
const NotificationConfig = ({
|
|
||||||
notifications,
|
|
||||||
setMonitor,
|
|
||||||
setNotifications,
|
|
||||||
//FieldWrapper's props
|
|
||||||
gap,
|
|
||||||
labelMb,
|
|
||||||
labelFontWeight,
|
|
||||||
labelVariant,
|
|
||||||
labelSx = {},
|
|
||||||
sx = {},
|
|
||||||
}) => {
|
|
||||||
// Local state
|
|
||||||
const [notificationsSearch, setNotificationsSearch] = useState("");
|
|
||||||
const [selectedNotifications, setSelectedNotifications] = useState([]);
|
|
||||||
|
|
||||||
const handleSearch = (value) => {
|
|
||||||
setSelectedNotifications(value);
|
|
||||||
setMonitor((prev) => {
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
notifications: value.map((notification) => notification._id),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handlers
|
|
||||||
const handleDelete = (id) => {
|
|
||||||
const updatedNotifications = selectedNotifications.filter(
|
|
||||||
(notification) => notification._id !== id
|
|
||||||
);
|
|
||||||
|
|
||||||
setSelectedNotifications(updatedNotifications);
|
|
||||||
setMonitor((prev) => {
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
notifications: updatedNotifications.map((notification) => notification._id),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Setup
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (setNotifications) {
|
|
||||||
const toSet = setNotifications.map((notification) => {
|
|
||||||
return notifications.find((n) => n._id === notification);
|
|
||||||
});
|
|
||||||
setSelectedNotifications(toSet);
|
|
||||||
}
|
|
||||||
}, [setNotifications, notifications]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack gap={theme.spacing(6)}>
|
|
||||||
<Search
|
|
||||||
type="notifications"
|
|
||||||
label="Notifications"
|
|
||||||
options={notifications}
|
|
||||||
filteredBy="notificationName"
|
|
||||||
multiple={true}
|
|
||||||
value={selectedNotifications}
|
|
||||||
inputValue={notificationsSearch}
|
|
||||||
handleInputChange={setNotificationsSearch}
|
|
||||||
handleChange={(value) => {
|
|
||||||
handleSearch(value);
|
|
||||||
}}
|
|
||||||
labelMb={labelMb}
|
|
||||||
labelVariant={labelVariant}
|
|
||||||
labelFontWeight={labelFontWeight}
|
|
||||||
labelSx={labelSx}
|
|
||||||
gap={gap}
|
|
||||||
sx={{
|
|
||||||
...sx,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack
|
|
||||||
flex={1}
|
|
||||||
width="100%"
|
|
||||||
>
|
|
||||||
{selectedNotifications.map((notification, index) => (
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
alignItems="center"
|
|
||||||
key={notification._id}
|
|
||||||
width="100%"
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
flexGrow={1} // <-- This will take up all available horizontal space
|
|
||||||
>
|
|
||||||
{notification.notificationName}
|
|
||||||
</Typography>
|
|
||||||
<DeleteOutlineRoundedIcon
|
|
||||||
onClick={() => {
|
|
||||||
handleDelete(notification._id);
|
|
||||||
}}
|
|
||||||
sx={{ cursor: "pointer" }}
|
|
||||||
/>
|
|
||||||
{index < selectedNotifications.length - 1 && <Divider />}
|
|
||||||
</Stack>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
NotificationConfig.propTypes = {
|
|
||||||
notifications: PropTypes.array,
|
|
||||||
setMonitor: PropTypes.func,
|
|
||||||
setNotifications: PropTypes.array,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NotificationConfig;
|
|
||||||
-419
@@ -1,419 +0,0 @@
|
|||||||
import { useState, useMemo, useEffect, useCallback } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogActions,
|
|
||||||
Button,
|
|
||||||
Typography,
|
|
||||||
Box,
|
|
||||||
Tabs,
|
|
||||||
Tab,
|
|
||||||
Stack,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { useTheme } from "@emotion/react";
|
|
||||||
import TabPanel from "./TabPanel.jsx";
|
|
||||||
import TabComponent from "./TabComponent.jsx";
|
|
||||||
import useNotifications from "../Hooks/useNotification.js";
|
|
||||||
|
|
||||||
// Define constants for notification types to avoid magic values
|
|
||||||
const NOTIFICATION_TYPES = {
|
|
||||||
SLACK: "slack",
|
|
||||||
DISCORD: "discord",
|
|
||||||
TELEGRAM: "telegram",
|
|
||||||
WEBHOOK: "webhook",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Define constants for field IDs
|
|
||||||
const FIELD_IDS = {
|
|
||||||
WEBHOOK: "webhook",
|
|
||||||
TOKEN: "token",
|
|
||||||
CHAT_ID: "chatId",
|
|
||||||
URL: "url",
|
|
||||||
};
|
|
||||||
|
|
||||||
const NotificationIntegrationModal = ({
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
monitor,
|
|
||||||
setMonitor,
|
|
||||||
// Optional prop to configure available notification types
|
|
||||||
notificationTypes = null,
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const theme = useTheme();
|
|
||||||
const [tabValue, setTabValue] = useState(0);
|
|
||||||
|
|
||||||
const [loading, _, sendTestNotification] = useNotifications();
|
|
||||||
|
|
||||||
// Helper to get the field state key with error handling
|
|
||||||
const getFieldKey = useCallback(
|
|
||||||
(typeId, fieldId) => {
|
|
||||||
if (typeof typeId !== "string" || typeId === "") {
|
|
||||||
throw new Error(t("errorInvalidTypeId"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof fieldId !== "string" || fieldId === "") {
|
|
||||||
throw new Error(t("errorInvalidFieldId"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`;
|
|
||||||
},
|
|
||||||
[t]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Define notification types
|
|
||||||
const DEFAULT_NOTIFICATION_TYPES = [
|
|
||||||
{
|
|
||||||
id: NOTIFICATION_TYPES.SLACK,
|
|
||||||
label: t("notifications.slack.label"),
|
|
||||||
description: t("notifications.slack.description"),
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
id: FIELD_IDS.WEBHOOK,
|
|
||||||
label: t("notifications.slack.webhookLabel"),
|
|
||||||
placeholder: t("notifications.slack.webhookPlaceholder"),
|
|
||||||
type: "text",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: NOTIFICATION_TYPES.DISCORD,
|
|
||||||
label: t("notifications.discord.label"),
|
|
||||||
description: t("notifications.discord.description"),
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
id: FIELD_IDS.WEBHOOK,
|
|
||||||
label: t("notifications.discord.webhookLabel"),
|
|
||||||
placeholder: t("notifications.discord.webhookPlaceholder"),
|
|
||||||
type: "text",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: NOTIFICATION_TYPES.TELEGRAM,
|
|
||||||
label: t("notifications.telegram.label"),
|
|
||||||
description: t("notifications.telegram.description"),
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
id: FIELD_IDS.TOKEN,
|
|
||||||
label: t("notifications.telegram.tokenLabel"),
|
|
||||||
placeholder: t("notifications.telegram.tokenPlaceholder"),
|
|
||||||
type: "text",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: FIELD_IDS.CHAT_ID,
|
|
||||||
label: t("notifications.telegram.chatIdLabel"),
|
|
||||||
placeholder: t("notifications.telegram.chatIdPlaceholder"),
|
|
||||||
type: "text",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: NOTIFICATION_TYPES.WEBHOOK,
|
|
||||||
label: t("notifications.webhook.label"),
|
|
||||||
description: t("notifications.webhook.description"),
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
id: FIELD_IDS.URL,
|
|
||||||
label: t("notifications.webhook.urlLabel"),
|
|
||||||
placeholder: t("notifications.webhook.urlPlaceholder"),
|
|
||||||
type: "text",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Use provided notification types or default to our translated ones
|
|
||||||
const activeNotificationTypes = notificationTypes || DEFAULT_NOTIFICATION_TYPES;
|
|
||||||
|
|
||||||
// Memoized function to initialize integrations state
|
|
||||||
const initialIntegrationsState = useMemo(() => {
|
|
||||||
const state = {};
|
|
||||||
|
|
||||||
activeNotificationTypes.forEach((type) => {
|
|
||||||
// Add enabled flag for each notification type
|
|
||||||
state[type.id] = false;
|
|
||||||
|
|
||||||
// Add state for each field in the notification type
|
|
||||||
type.fields.forEach((field) => {
|
|
||||||
const fieldKey = getFieldKey(type.id, field.id);
|
|
||||||
state[fieldKey] = "";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}, [activeNotificationTypes, getFieldKey]); // Only recompute when these dependencies change
|
|
||||||
|
|
||||||
const [integrations, setIntegrations] = useState(initialIntegrationsState);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
const extractNotificationValues = () => {
|
|
||||||
const values = {};
|
|
||||||
|
|
||||||
if (!monitor?.notifications || !Array.isArray(monitor.notifications)) {
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
monitor.notifications.forEach((notification) => {
|
|
||||||
// Handle notification based on its structure
|
|
||||||
if (notification.type === "webhook" && notification.platform) {
|
|
||||||
if (typeof notification.config === "undefined") return;
|
|
||||||
const platform = notification.platform;
|
|
||||||
values[platform] = true; // Set platform as enabled
|
|
||||||
|
|
||||||
// Extract configuration based on platform
|
|
||||||
switch (platform) {
|
|
||||||
case NOTIFICATION_TYPES.SLACK:
|
|
||||||
case NOTIFICATION_TYPES.DISCORD:
|
|
||||||
if (notification.config.webhookUrl) {
|
|
||||||
values[getFieldKey(platform, FIELD_IDS.WEBHOOK)] =
|
|
||||||
notification.config.webhookUrl;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case NOTIFICATION_TYPES.TELEGRAM:
|
|
||||||
if (notification.config.botToken) {
|
|
||||||
values[getFieldKey(platform, FIELD_IDS.TOKEN)] =
|
|
||||||
notification.config.botToken;
|
|
||||||
}
|
|
||||||
if (notification.config.chatId) {
|
|
||||||
values[getFieldKey(platform, FIELD_IDS.CHAT_ID)] =
|
|
||||||
notification.config.chatId;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case NOTIFICATION_TYPES.WEBHOOK:
|
|
||||||
if (notification.config.webhookUrl) {
|
|
||||||
values[getFieldKey(platform, FIELD_IDS.URL)] =
|
|
||||||
notification.config.webhookUrl;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return values;
|
|
||||||
};
|
|
||||||
|
|
||||||
const extractedValues = extractNotificationValues();
|
|
||||||
setIntegrations((prev) => ({
|
|
||||||
...prev,
|
|
||||||
...extractedValues,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}, [open, monitor, getFieldKey]);
|
|
||||||
|
|
||||||
const handleChangeTab = (event, newValue) => {
|
|
||||||
setTabValue(newValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleIntegrationChange = (type, checked) => {
|
|
||||||
setIntegrations((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[type]: checked,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInputChange = (type, value) => {
|
|
||||||
setIntegrations((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[type]: value,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTestNotification = async (type) => {
|
|
||||||
// Get the notification type details
|
|
||||||
const notificationType = activeNotificationTypes.find((t) => t.id === type);
|
|
||||||
|
|
||||||
if (typeof notificationType === "undefined") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare config object based on notification type
|
|
||||||
const config = {};
|
|
||||||
|
|
||||||
// Add each field value to the config object
|
|
||||||
notificationType.fields.forEach((field) => {
|
|
||||||
const fieldKey = getFieldKey(type, field.id);
|
|
||||||
config[field.id] = integrations[fieldKey];
|
|
||||||
});
|
|
||||||
|
|
||||||
await sendTestNotification(type, config);
|
|
||||||
};
|
|
||||||
|
|
||||||
// In NotificationIntegrationModal.jsx, update the handleSave function:
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
// Get existing notifications
|
|
||||||
const notifications = [...(monitor?.notifications || [])];
|
|
||||||
|
|
||||||
// Get all notification types IDs
|
|
||||||
const existingTypes = activeNotificationTypes.map((type) => type.id);
|
|
||||||
|
|
||||||
// Filter out notifications that are configurable in this modal
|
|
||||||
const filteredNotifications = notifications.filter((notification) => {
|
|
||||||
if (notification.platform) {
|
|
||||||
return !existingTypes.includes(notification.platform);
|
|
||||||
}
|
|
||||||
|
|
||||||
return !existingTypes.includes(notification.type);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add each enabled notification with its configured fields
|
|
||||||
activeNotificationTypes.forEach((type) => {
|
|
||||||
if (integrations[type.id]) {
|
|
||||||
let notificationObject = {
|
|
||||||
type: "webhook",
|
|
||||||
platform: type.id, // Set platform to identify the specific service
|
|
||||||
config: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Configure based on notification type
|
|
||||||
switch (type.id) {
|
|
||||||
case "slack":
|
|
||||||
case "discord":
|
|
||||||
notificationObject.config.webhookUrl =
|
|
||||||
integrations[getFieldKey(type.id, "webhook")];
|
|
||||||
break;
|
|
||||||
case "telegram":
|
|
||||||
notificationObject.config.botToken =
|
|
||||||
integrations[getFieldKey(type.id, "token")];
|
|
||||||
notificationObject.config.chatId =
|
|
||||||
integrations[getFieldKey(type.id, "chatId")];
|
|
||||||
break;
|
|
||||||
case "webhook":
|
|
||||||
notificationObject.config.webhookUrl =
|
|
||||||
integrations[getFieldKey(type.id, "url")];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
filteredNotifications.push(notificationObject);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update monitor with new notifications
|
|
||||||
setMonitor((prev) => ({
|
|
||||||
...prev,
|
|
||||||
notifications: filteredNotifications,
|
|
||||||
}));
|
|
||||||
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
open={open}
|
|
||||||
onClose={onClose}
|
|
||||||
fullWidth
|
|
||||||
maxWidth="md"
|
|
||||||
sx={{
|
|
||||||
"& .MuiDialog-paper": {
|
|
||||||
width: `calc(80% - ${theme.spacing(40)})`,
|
|
||||||
maxWidth: `${theme.breakpoints.values.md - 70}px`,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogContent>
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
sx={{
|
|
||||||
height: `calc(30vh - ${theme.spacing(20)})`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Left sidebar with tabs */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
borderRight: 1,
|
|
||||||
borderColor: theme.palette.primary.lowContrast,
|
|
||||||
width: "30%",
|
|
||||||
maxWidth: theme.spacing(120),
|
|
||||||
pr: theme.spacing(10),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="h2">
|
|
||||||
{t("notifications.addOrEditNotifications")}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Tabs
|
|
||||||
orientation="vertical"
|
|
||||||
variant="scrollable"
|
|
||||||
value={tabValue}
|
|
||||||
onChange={handleChangeTab}
|
|
||||||
aria-label="Notification tabs"
|
|
||||||
>
|
|
||||||
{activeNotificationTypes.map((type) => (
|
|
||||||
<Tab
|
|
||||||
key={type.id}
|
|
||||||
label={type.label}
|
|
||||||
orientation="vertical"
|
|
||||||
disableRipple
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Tabs>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Right side content */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
flex: 1,
|
|
||||||
pl: theme.spacing(7.5),
|
|
||||||
overflowY: "auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{activeNotificationTypes.map((type, index) => (
|
|
||||||
<TabPanel
|
|
||||||
key={type.id}
|
|
||||||
value={tabValue}
|
|
||||||
index={index}
|
|
||||||
>
|
|
||||||
<TabComponent
|
|
||||||
type={type}
|
|
||||||
integrations={integrations}
|
|
||||||
handleIntegrationChange={handleIntegrationChange}
|
|
||||||
handleInputChange={handleInputChange}
|
|
||||||
handleTestNotification={handleTestNotification}
|
|
||||||
isLoading={loading}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions
|
|
||||||
sx={{
|
|
||||||
p: theme.spacing(4),
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
mb: theme.spacing(5),
|
|
||||||
mr: theme.spacing(5),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="accent"
|
|
||||||
onClick={handleSave}
|
|
||||||
loading={loading}
|
|
||||||
sx={{
|
|
||||||
width: "auto",
|
|
||||||
minWidth: theme.spacing(60),
|
|
||||||
px: theme.spacing(8),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("commonSave")}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
NotificationIntegrationModal.propTypes = {
|
|
||||||
open: PropTypes.bool.isRequired,
|
|
||||||
onClose: PropTypes.func.isRequired,
|
|
||||||
monitor: PropTypes.object.isRequired,
|
|
||||||
setMonitor: PropTypes.func.isRequired,
|
|
||||||
notificationTypes: PropTypes.array,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NotificationIntegrationModal;
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Typography, Box, Button, CircularProgress } from "@mui/material";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useTheme } from "@emotion/react";
|
|
||||||
import TextInput from "../../Inputs/TextInput/index.jsx";
|
|
||||||
import Checkbox from "../../Inputs/Checkbox/index.jsx";
|
|
||||||
|
|
||||||
const TabComponent = ({
|
|
||||||
type,
|
|
||||||
integrations,
|
|
||||||
handleIntegrationChange,
|
|
||||||
handleInputChange,
|
|
||||||
handleTestNotification,
|
|
||||||
isLoading,
|
|
||||||
}) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
// Helper to get the field state key (e.g., slackWebhook, telegramToken)
|
|
||||||
const getFieldKey = (typeId, fieldId) => {
|
|
||||||
return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if all fields have values to enable test button
|
|
||||||
const areAllFieldsFilled = () => {
|
|
||||||
return type.fields.every((field) => {
|
|
||||||
const fieldKey = getFieldKey(type.id, field.id);
|
|
||||||
return integrations[fieldKey];
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Typography
|
|
||||||
variant="subtitle1"
|
|
||||||
component="h4"
|
|
||||||
sx={{
|
|
||||||
fontWeight: "bold",
|
|
||||||
color: theme.palette.primary.contrastTextSecondary,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{type.label}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
mt: theme.spacing(0.5),
|
|
||||||
mb: theme.spacing(1.5),
|
|
||||||
color: theme.palette.primary.contrastTextTertiary,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{type.description}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Box sx={{ pl: theme.spacing(1.5) }}>
|
|
||||||
<Checkbox
|
|
||||||
id={`enable-${type.id}`}
|
|
||||||
label={t("notifications.enableNotifications", { platform: type.label })}
|
|
||||||
isChecked={integrations[type.id]}
|
|
||||||
onChange={(e) => handleIntegrationChange(type.id, e.target.checked)}
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{type.fields.map((field) => {
|
|
||||||
const fieldKey = getFieldKey(type.id, field.id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
key={field.id}
|
|
||||||
sx={{ mt: theme.spacing(1) }}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
mb: theme.spacing(2),
|
|
||||||
fontWeight: "bold",
|
|
||||||
color: theme.palette.primary.contrastTextSecondary,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{field.label}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<TextInput
|
|
||||||
id={`${type.id}-${field.id}`}
|
|
||||||
type={field.type}
|
|
||||||
placeholder={field.placeholder}
|
|
||||||
value={integrations[fieldKey]}
|
|
||||||
onChange={(e) => handleInputChange(fieldKey, e.target.value)}
|
|
||||||
disabled={!integrations[type.id] || isLoading}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<Box sx={{ mt: theme.spacing(1) }}>
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
color="info"
|
|
||||||
onClick={() => handleTestNotification(type.id)}
|
|
||||||
disabled={!integrations[type.id] || !areAllFieldsFilled() || isLoading}
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<CircularProgress
|
|
||||||
size={theme.spacing(8)}
|
|
||||||
sx={{ mr: theme.spacing(1), color: theme.palette.accent.main }}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{t("notifications.testNotification")}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TabComponent;
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { Box } from "@mui/material";
|
|
||||||
import { useTheme } from "@emotion/react";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TabPanel component that displays content for the selected tab.
|
|
||||||
*
|
|
||||||
* @component
|
|
||||||
* @param {Object} props - The component props.
|
|
||||||
* @param {React.ReactNode} props.children - The content to be displayed when this tab panel is selected.
|
|
||||||
* @param {number} props.value - The currently selected tab value.
|
|
||||||
* @param {number} props.index - The index of this specific tab panel.
|
|
||||||
* @param {Object} props.other - Any additional props to be spread to the root element.
|
|
||||||
* @returns {React.ReactElement|null} The rendered tab panel or null if not selected.
|
|
||||||
*/
|
|
||||||
function TabPanel({ children, value, index, ...other }) {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="tabpanel"
|
|
||||||
hidden={value !== index}
|
|
||||||
id={`notification-tabpanel-${index}`}
|
|
||||||
aria-labelledby={`notification-tab-${index}`}
|
|
||||||
{...other}
|
|
||||||
>
|
|
||||||
{value === index && <Box sx={{ pt: theme.spacing(3) }}>{children}</Box>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
TabPanel.propTypes = {
|
|
||||||
children: PropTypes.node,
|
|
||||||
|
|
||||||
index: PropTypes.number.isRequired,
|
|
||||||
|
|
||||||
value: PropTypes.number.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TabPanel;
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { networkService } from "../../../../Utils/NetworkService.js";
|
|
||||||
import { createToast } from "../../../../Utils/toastUtils.jsx";
|
|
||||||
|
|
||||||
// Define constants for notification types to avoid magic values
|
|
||||||
const NOTIFICATION_TYPES = {
|
|
||||||
SLACK: "slack",
|
|
||||||
DISCORD: "discord",
|
|
||||||
TELEGRAM: "telegram",
|
|
||||||
WEBHOOK: "webhook",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Define constants for field IDs
|
|
||||||
const FIELD_IDS = {
|
|
||||||
WEBHOOK: "webhook",
|
|
||||||
TOKEN: "token",
|
|
||||||
CHAT_ID: "chatId",
|
|
||||||
URL: "url",
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom hook for notification-related operations
|
|
||||||
*/
|
|
||||||
const useNotifications = () => {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState(undefined);
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a test notification
|
|
||||||
* @param {string} type - The notification type (slack, discord, telegram, webhook)
|
|
||||||
* @param {object} config - Configuration object with necessary params
|
|
||||||
*/
|
|
||||||
const sendTestNotification = async (type, config) => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(undefined);
|
|
||||||
|
|
||||||
// Validation based on notification type
|
|
||||||
let payload = { platform: type };
|
|
||||||
let isValid = true;
|
|
||||||
let errorMessage = "";
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case NOTIFICATION_TYPES.SLACK:
|
|
||||||
payload.webhookUrl = config.webhook;
|
|
||||||
if (typeof payload.webhookUrl === "undefined" || payload.webhookUrl === "") {
|
|
||||||
isValid = false;
|
|
||||||
errorMessage = t("notifications.slack.webhookRequired");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NOTIFICATION_TYPES.DISCORD:
|
|
||||||
payload.webhookUrl = config.webhook;
|
|
||||||
if (typeof payload.webhookUrl === "undefined" || payload.webhookUrl === "") {
|
|
||||||
isValid = false;
|
|
||||||
errorMessage = t("notifications.discord.webhookRequired");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NOTIFICATION_TYPES.TELEGRAM:
|
|
||||||
payload.botToken = config.token;
|
|
||||||
payload.chatId = config.chatId;
|
|
||||||
if (
|
|
||||||
typeof payload.botToken === "undefined" ||
|
|
||||||
payload.botToken === "" ||
|
|
||||||
typeof payload.chatId === "undefined" ||
|
|
||||||
payload.chatId === ""
|
|
||||||
) {
|
|
||||||
isValid = false;
|
|
||||||
errorMessage = t("notifications.telegram.fieldsRequired");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NOTIFICATION_TYPES.WEBHOOK:
|
|
||||||
payload.webhookUrl = config.url;
|
|
||||||
payload.platform = NOTIFICATION_TYPES.SLACK;
|
|
||||||
if (typeof payload.webhookUrl === "undefined" || payload.webhookUrl === "") {
|
|
||||||
isValid = false;
|
|
||||||
errorMessage = t("notifications.webhook.urlRequired");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
isValid = false;
|
|
||||||
errorMessage = t("notifications.unsupportedType");
|
|
||||||
}
|
|
||||||
|
|
||||||
// If validation fails, show error and return
|
|
||||||
if (isValid === false) {
|
|
||||||
createToast({
|
|
||||||
body: errorMessage,
|
|
||||||
variant: "error",
|
|
||||||
});
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await networkService.testNotification({
|
|
||||||
platform: type,
|
|
||||||
payload: payload,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data.success === true) {
|
|
||||||
createToast({
|
|
||||||
body: t("notifications.testSuccess"),
|
|
||||||
variant: "info",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new Error(response.data.msg || t("notifications.testFailed"));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
const errorMsg =
|
|
||||||
error.response?.data?.msg || error.message || t("notifications.networkError");
|
|
||||||
createToast({
|
|
||||||
body: `${t("notifications.testFailed")}: ${errorMsg}`,
|
|
||||||
variant: "error",
|
|
||||||
});
|
|
||||||
setError(errorMsg);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return [loading, error, sendTestNotification];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useNotifications;
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { Box, IconButton, LinearProgress, Stack, Typography } from "@mui/material";
|
import { Box, IconButton, LinearProgress, Stack, Typography } from "@mui/material";
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import Icon from "../Icon";
|
||||||
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,7 +50,10 @@ const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) =>
|
|||||||
alignItems={error ? "center" : "flex-start"}
|
alignItems={error ? "center" : "flex-start"}
|
||||||
>
|
>
|
||||||
{error ? (
|
{error ? (
|
||||||
<ErrorOutlineOutlinedIcon />
|
<Icon
|
||||||
|
name="AlertTriangle"
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
) : icon ? (
|
) : icon ? (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -127,10 +129,9 @@ const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<CloseIcon
|
<Icon
|
||||||
sx={{
|
name="X"
|
||||||
fontSize: "20px",
|
size={20}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Step, Stepper, StepLabel, Typography } from "@mui/material";
|
import { Step, Stepper, StepLabel, Typography } from "@mui/material";
|
||||||
import RadioButtonCheckedIcon from "@mui/icons-material/RadioButtonChecked";
|
import Icon from "../Icon";
|
||||||
import CheckCircle from "@mui/icons-material/CheckCircle";
|
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
const CustomStepIcon = (props) => {
|
const CustomStepIcon = (props) => {
|
||||||
const { completed, active } = props;
|
const { completed, active } = props;
|
||||||
return completed ? (
|
return completed ? (
|
||||||
<CheckCircle color="accent" />
|
<Icon
|
||||||
|
name="CheckCircle2"
|
||||||
|
size={24}
|
||||||
|
color="accent.main"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<RadioButtonCheckedIcon color={active ? "accent" : "disabled"} />
|
<Icon
|
||||||
|
name="Circle"
|
||||||
|
size={24}
|
||||||
|
color={active ? "accent.main" : "text.disabled"}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import html2canvas from "html2canvas";
|
import html2canvas from "html2canvas";
|
||||||
import ShareIcon from "@mui/icons-material/Share";
|
import Icon from "../Icon";
|
||||||
import { Button } from "@mui/material";
|
import { Button } from "@mui/material";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
|
|
||||||
@@ -55,7 +55,13 @@ const ShareComponent = ({ elementToCapture, fileName = "screenshot" }) => {
|
|||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
startIcon={<ShareIcon sx={{ color: theme.palette.success.main }} />}
|
startIcon={
|
||||||
|
<Icon
|
||||||
|
name="Share2"
|
||||||
|
size={18}
|
||||||
|
color="success.main"
|
||||||
|
/>
|
||||||
|
}
|
||||||
color="success"
|
color="success"
|
||||||
onClick={captureAndShare}
|
onClick={captureAndShare}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import Avatar from "../../Avatar/index.jsx";
|
|||||||
import Menu from "@mui/material/Menu";
|
import Menu from "@mui/material/Menu";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
import DotsVertical from "../../../../assets/icons/dots-vertical.svg?react";
|
import Icon from "../../Icon";
|
||||||
import LogoutSvg from "../../../../assets/icons/logout.svg?react";
|
|
||||||
|
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
@@ -81,6 +80,10 @@ const AuthFooter = ({ collapsed, accountMenuItems }) => {
|
|||||||
gap: theme.spacing(2),
|
gap: theme.spacing(2),
|
||||||
borderRadius: theme.shape.borderRadius,
|
borderRadius: theme.shape.borderRadius,
|
||||||
pl: theme.spacing(4),
|
pl: theme.spacing(4),
|
||||||
|
color: theme.palette.primary.contrastTextTertiary,
|
||||||
|
"& svg": {
|
||||||
|
stroke: theme.palette.primary.contrastTextTertiary,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
@@ -160,19 +163,14 @@ const AuthFooter = ({ collapsed, accountMenuItems }) => {
|
|||||||
ml: "50px",
|
ml: "50px",
|
||||||
"&:focus": { outline: "none" },
|
"&:focus": { outline: "none" },
|
||||||
alignSelf: "center",
|
alignSelf: "center",
|
||||||
|
|
||||||
"& svg": {
|
|
||||||
width: "22px",
|
|
||||||
height: "22px",
|
|
||||||
},
|
|
||||||
"& svg path": {
|
|
||||||
/* Vertical three dots */
|
|
||||||
stroke: theme.palette.primary.contrastTextTertiary,
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
onClick={(event) => openPopup(event)}
|
onClick={(event) => openPopup(event)}
|
||||||
>
|
>
|
||||||
<DotsVertical />
|
<Icon
|
||||||
|
name="MoreVertical"
|
||||||
|
size={22}
|
||||||
|
color="primary.contrastTextTertiary"
|
||||||
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -256,12 +254,13 @@ const AuthFooter = ({ collapsed, accountMenuItems }) => {
|
|||||||
gap: theme.spacing(4),
|
gap: theme.spacing(4),
|
||||||
borderRadius: theme.shape.borderRadius,
|
borderRadius: theme.shape.borderRadius,
|
||||||
pl: theme.spacing(4),
|
pl: theme.spacing(4),
|
||||||
"& svg path": {
|
|
||||||
stroke: theme.palette.primary.contrastTextTertiary,
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LogoutSvg />
|
<Icon
|
||||||
|
name="LogOut"
|
||||||
|
size={20}
|
||||||
|
color="primary.contrastTextTertiary"
|
||||||
|
/>
|
||||||
{t("menu.logOut", "Log out")}
|
{t("menu.logOut", "Log out")}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@@ -52,12 +52,11 @@ const NavItem = ({ item, collapsed, selected, onClick }) => {
|
|||||||
<ListItemIcon
|
<ListItemIcon
|
||||||
sx={{
|
sx={{
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
|
color: iconStroke,
|
||||||
"& svg": {
|
"& svg": {
|
||||||
height: 20,
|
height: 20,
|
||||||
width: 20,
|
width: 20,
|
||||||
opacity: 0.81,
|
opacity: 0.81,
|
||||||
},
|
|
||||||
"& svg path": {
|
|
||||||
stroke: iconStroke,
|
stroke: iconStroke,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -8,28 +8,13 @@ import NavItem from "./components/navItem.jsx";
|
|||||||
import AuthFooter from "./components/authFooter.jsx";
|
import AuthFooter from "./components/authFooter.jsx";
|
||||||
|
|
||||||
import StarPrompt from "../StarPrompt/index.jsx";
|
import StarPrompt from "../StarPrompt/index.jsx";
|
||||||
import LockSvg from "../../../assets/icons/lock.svg?react";
|
import Icon from "../Icon";
|
||||||
import UserSvg from "../../../assets/icons/user.svg?react";
|
|
||||||
import TeamSvg from "../../../assets/icons/user-two.svg?react";
|
|
||||||
import Support from "../../../assets/icons/support.svg?react";
|
|
||||||
import Maintenance from "../../../assets/icons/maintenance.svg?react";
|
|
||||||
import Monitors from "../../../assets/icons/monitors.svg?react";
|
|
||||||
import Incidents from "../../../assets/icons/incidents.svg?react";
|
|
||||||
import Integrations from "../../../assets/icons/integrations.svg?react";
|
|
||||||
import PageSpeed from "../../../assets/icons/page-speed.svg?react";
|
|
||||||
import Settings from "../../../assets/icons/settings.svg?react";
|
|
||||||
import ChangeLog from "../../../assets/icons/changeLog.svg?react";
|
|
||||||
import Docs from "../../../assets/icons/docs.svg?react";
|
|
||||||
import StatusPages from "../../../assets/icons/status-pages.svg?react";
|
|
||||||
import Discussions from "../../../assets/icons/discussions.svg?react";
|
|
||||||
import Notifications from "../../../assets/icons/notifications.svg?react";
|
|
||||||
import Logs from "../../../assets/icons/logs.svg?react";
|
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
|
import { useSidebar } from "@/Hooks/useSidebar.js";
|
||||||
|
|
||||||
const URL_MAP = {
|
const URL_MAP = {
|
||||||
support: "https://discord.com/invite/NAb6H3UTjK",
|
support: "https://discord.com/invite/NAb6H3UTjK",
|
||||||
@@ -39,52 +24,48 @@ const URL_MAP = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getMenu = (t) => [
|
const getMenu = (t) => [
|
||||||
{ name: t("menu.uptime"), path: "uptime", icon: <Monitors /> },
|
{ name: t("menu.uptime"), path: "uptime", icon: <Icon name="Globe" /> },
|
||||||
{ name: t("menu.pagespeed"), path: "pagespeed", icon: <PageSpeed /> },
|
{ name: t("menu.pagespeed"), path: "pagespeed", icon: <Icon name="Gauge" /> },
|
||||||
|
{ name: t("menu.infrastructure"), path: "infrastructure", icon: <Icon name="Link" /> },
|
||||||
{ name: t("menu.infrastructure"), path: "infrastructure", icon: <Integrations /> },
|
|
||||||
{
|
{
|
||||||
name: t("menu.notifications"),
|
name: t("menu.notifications"),
|
||||||
path: "notifications",
|
path: "notifications",
|
||||||
icon: <Notifications />,
|
icon: <Icon name="Bell" />,
|
||||||
},
|
},
|
||||||
{ name: t("menu.checks"), path: "checks", icon: <Docs /> },
|
{ name: t("menu.checks"), path: "checks", icon: <Icon name="FileText" /> },
|
||||||
{ name: t("menu.incidents"), path: "incidents", icon: <Incidents /> },
|
{ name: t("menu.incidents"), path: "incidents", icon: <Icon name="AlertTriangle" /> },
|
||||||
|
{ name: t("menu.statusPages"), path: "status", icon: <Icon name="Wifi" /> },
|
||||||
{ name: t("menu.statusPages"), path: "status", icon: <StatusPages /> },
|
{ name: t("menu.maintenance"), path: "maintenance", icon: <Icon name="Wrench" /> },
|
||||||
{ name: t("menu.maintenance"), path: "maintenance", icon: <Maintenance /> },
|
{ name: t("menu.logs"), path: "logs", icon: <Icon name="Database" /> },
|
||||||
{ name: t("menu.logs"), path: "logs", icon: <Logs /> },
|
|
||||||
|
|
||||||
{
|
{
|
||||||
name: t("menu.settings"),
|
name: t("menu.settings"),
|
||||||
icon: <Settings />,
|
icon: <Icon name="Settings" />,
|
||||||
path: "settings",
|
path: "settings",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const getOtherMenuItems = (t) => [
|
const getOtherMenuItems = (t) => [
|
||||||
{ name: t("menu.support"), path: "support", icon: <Support /> },
|
{ name: t("menu.support"), path: "support", icon: <Icon name="HelpCircle" /> },
|
||||||
{
|
{
|
||||||
name: t("menu.discussions"),
|
name: t("menu.discussions"),
|
||||||
path: "discussions",
|
path: "discussions",
|
||||||
icon: <Discussions />,
|
icon: <Icon name="MessageCircle" />,
|
||||||
},
|
},
|
||||||
{ name: t("menu.docs"), path: "docs", icon: <Docs /> },
|
{ name: t("menu.docs"), path: "docs", icon: <Icon name="FileText" /> },
|
||||||
{ name: t("menu.changelog"), path: "changelog", icon: <ChangeLog /> },
|
{ name: t("menu.changelog"), path: "changelog", icon: <Icon name="Code" /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
const getAccountMenuItems = (t) => [
|
const getAccountMenuItems = (t) => [
|
||||||
{ name: t("menu.profile"), path: "account/profile", icon: <UserSvg /> },
|
{ name: t("menu.profile"), path: "account/profile", icon: <Icon name="User" /> },
|
||||||
{ name: t("menu.password"), path: "account/password", icon: <LockSvg /> },
|
{ name: t("menu.password"), path: "account/password", icon: <Icon name="Lock" /> },
|
||||||
{ name: t("menu.team"), path: "account/team", icon: <TeamSvg /> },
|
{ name: t("menu.team"), path: "account/team", icon: <Icon name="Users" /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
const Sidebar = () => {
|
const Sidebar = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
// Redux state
|
const { collapsed, width, transition } = useSidebar();
|
||||||
const collapsed = useSelector((state) => state.ui.sidebar?.collapsed ?? false);
|
|
||||||
|
|
||||||
const menu = getMenu(t);
|
const menu = getMenu(t);
|
||||||
const otherMenuItems = getOtherMenuItems(t);
|
const otherMenuItems = getOtherMenuItems(t);
|
||||||
@@ -93,20 +74,19 @@ const Sidebar = () => {
|
|||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
height="100vh"
|
height="100vh"
|
||||||
width={
|
width={width}
|
||||||
collapsed
|
|
||||||
? "var(--env-var-side-bar-collapsed-width)"
|
|
||||||
: "var(--env-var-side-bar-width)"
|
|
||||||
}
|
|
||||||
component="aside"
|
component="aside"
|
||||||
position="sticky"
|
position="fixed"
|
||||||
top={0}
|
top={0}
|
||||||
borderRight={`1px solid ${theme.palette.primary.lowContrast}`}
|
left={0}
|
||||||
paddingTop={theme.spacing(6)}
|
paddingTop={theme.spacing(6)}
|
||||||
paddingBottom={theme.spacing(6)}
|
paddingBottom={theme.spacing(6)}
|
||||||
gap={theme.spacing(6)}
|
gap={theme.spacing(6)}
|
||||||
sx={{
|
sx={{
|
||||||
transition: "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)",
|
transition,
|
||||||
|
backgroundColor: theme.palette.background.main,
|
||||||
|
borderRight: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||||
|
zIndex: 1000,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CollapseButton collapsed={collapsed} />
|
<CollapseButton collapsed={collapsed} />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Typography, IconButton, Stack, Box } from "@mui/material";
|
import { Typography, IconButton, Stack, Box } from "@mui/material";
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import Icon from "../Icon";
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -67,7 +67,10 @@ const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" })
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CloseIcon sx={{ fontSize: "1.25rem" }} />
|
<Icon
|
||||||
|
name="X"
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import PropTypes from "prop-types";
|
|||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import { Stack, TablePagination, Typography } from "@mui/material";
|
import { Stack, TablePagination, Typography } from "@mui/material";
|
||||||
import { TablePaginationActions } from "./Actions/index.jsx";
|
import { TablePaginationActions } from "./Actions/index.jsx";
|
||||||
import SelectorVertical from "../../../../assets/icons/selector-vertical.svg?react";
|
import Icon from "../../Icon";
|
||||||
|
|
||||||
Pagination.propTypes = {
|
Pagination.propTypes = {
|
||||||
paginationLabel: PropTypes.string, // Label for the pagination.
|
paginationLabel: PropTypes.string, // Label for the pagination.
|
||||||
@@ -100,7 +100,12 @@ function Pagination({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
inputProps: { id: "pagination-dropdown" },
|
inputProps: { id: "pagination-dropdown" },
|
||||||
IconComponent: SelectorVertical,
|
IconComponent: () => (
|
||||||
|
<Icon
|
||||||
|
name="ChevronsUpDown"
|
||||||
|
size={16}
|
||||||
|
/>
|
||||||
|
),
|
||||||
sx: {
|
sx: {
|
||||||
ml: theme.spacing(4),
|
ml: theme.spacing(4),
|
||||||
mr: theme.spacing(12),
|
mr: theme.spacing(12),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import PropTypes from "prop-types";
|
|||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
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();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -19,6 +19,7 @@ const TextLink = ({ text, linkText, href, target = "_self" }) => {
|
|||||||
to={href}
|
to={href}
|
||||||
component={RouterLink}
|
component={RouterLink}
|
||||||
target={target}
|
target={target}
|
||||||
|
state={state}
|
||||||
>
|
>
|
||||||
{linkText}
|
{linkText}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -30,6 +31,7 @@ TextLink.propTypes = {
|
|||||||
text: PropTypes.string,
|
text: PropTypes.string,
|
||||||
linkText: PropTypes.string,
|
linkText: PropTypes.string,
|
||||||
href: PropTypes.string,
|
href: PropTypes.string,
|
||||||
|
state: PropTypes.object,
|
||||||
target: PropTypes.string,
|
target: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,19 +3,31 @@ import IconButton from "@mui/material/IconButton";
|
|||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import ToastBody from "./body.jsx";
|
import ToastBody from "./body.jsx";
|
||||||
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
import Icon from "../Icon";
|
||||||
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
|
|
||||||
import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined";
|
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { useTheme } from "@emotion/react";
|
import { useTheme } from "@emotion/react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
info: <InfoOutlinedIcon />,
|
info: (
|
||||||
error: <ErrorOutlineOutlinedIcon />,
|
<Icon
|
||||||
warning: <WarningAmberOutlinedIcon />,
|
name="Info"
|
||||||
|
size={24}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
error: (
|
||||||
|
<Icon
|
||||||
|
name="AlertCircle"
|
||||||
|
size={24}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
warning: (
|
||||||
|
<Icon
|
||||||
|
name="AlertTriangle"
|
||||||
|
size={24}
|
||||||
|
/>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => {
|
const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => {
|
||||||
@@ -50,7 +62,10 @@ const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => {
|
|||||||
)}
|
)}
|
||||||
{title && (
|
{title && (
|
||||||
<IconButton onClick={onClick}>
|
<IconButton onClick={onClick}>
|
||||||
<CloseIcon />
|
<Icon
|
||||||
|
name="X"
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -63,7 +78,10 @@ const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => {
|
|||||||
<ToastBody body={body} />
|
<ToastBody body={body} />
|
||||||
{!title && (
|
{!title && (
|
||||||
<IconButton onClick={onClick}>
|
<IconButton onClick={onClick}>
|
||||||
<CloseIcon />
|
<Icon
|
||||||
|
name="X"
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import Menu from "@mui/material/Menu";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import { Settings } from "lucide-react";
|
||||||
|
|
||||||
|
export type ActionMenuItem = {
|
||||||
|
id: number | string;
|
||||||
|
label: React.ReactNode;
|
||||||
|
action: Function;
|
||||||
|
closeMenu?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ActionsMenu = ({ items }: { items: ActionMenuItem[] }) => {
|
||||||
|
const [anchorEl, setAnchorEl] = useState<null | any>(null);
|
||||||
|
const open = Boolean(anchorEl);
|
||||||
|
|
||||||
|
const handleClick = (e: React.MouseEvent<any>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setAnchorEl(e.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = (e: React.MouseEvent<any>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<IconButton onClick={handleClick}>
|
||||||
|
<Settings
|
||||||
|
size={20}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
<Menu
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
{items.map((item) => (
|
||||||
|
<MenuItem
|
||||||
|
key={item.id}
|
||||||
|
onClick={(e: React.MouseEvent<HTMLLIElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (item.closeMenu) handleClose(e);
|
||||||
|
item.action();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import type { CheckSnapshot } from "@/Types/Check";
|
||||||
|
import { getResponseColor } from "@/Utils/DataUtils";
|
||||||
|
import { HeatmapResponseTimeTooltip } from "@/Components/v2/common/charts/HeatmapResponseTimeTooltip";
|
||||||
|
import type { SxProps } from "@mui/material/styles";
|
||||||
|
import type { ResponsiveStyleValue } from "@mui/system";
|
||||||
|
|
||||||
|
interface PlaceholderCheck {
|
||||||
|
status: "placeholder";
|
||||||
|
responseTime: 0;
|
||||||
|
createdAt: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HeatmapResponseTimeProps {
|
||||||
|
checks: CheckSnapshot[];
|
||||||
|
gap?: ResponsiveStyleValue<number | string>;
|
||||||
|
availabilityCellSx?: SxProps;
|
||||||
|
responseCellSx?: SxProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HeatmapResponseTime = ({
|
||||||
|
checks,
|
||||||
|
gap,
|
||||||
|
availabilityCellSx,
|
||||||
|
responseCellSx,
|
||||||
|
}: HeatmapResponseTimeProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
if (!gap) {
|
||||||
|
gap = theme.spacing(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checks || checks.length === 0) return null;
|
||||||
|
|
||||||
|
const latestChecks = checks.slice(-25).reverse();
|
||||||
|
|
||||||
|
let data: Array<CheckSnapshot | PlaceholderCheck>;
|
||||||
|
if (latestChecks.length !== 25) {
|
||||||
|
const placeholders = Array(25 - latestChecks.length).fill({
|
||||||
|
status: "placeholder" as const,
|
||||||
|
});
|
||||||
|
data = [...latestChecks, ...placeholders];
|
||||||
|
} else {
|
||||||
|
data = latestChecks;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: "100%" }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(25, 1fr)",
|
||||||
|
gap: gap,
|
||||||
|
alignItems: "stretch",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{data.map((check, index) => {
|
||||||
|
const statusBg =
|
||||||
|
check.status === "placeholder"
|
||||||
|
? theme.palette.action.hover
|
||||||
|
: check.status === true
|
||||||
|
? theme.palette.success.light
|
||||||
|
: theme.palette.error.light;
|
||||||
|
const respBg =
|
||||||
|
check.status === "placeholder"
|
||||||
|
? theme.palette.action.hover
|
||||||
|
: getResponseColor(check.responseTime || 0, {
|
||||||
|
start: theme.palette.success.main,
|
||||||
|
mid: theme.palette.warning.main,
|
||||||
|
end: theme.palette.error.main,
|
||||||
|
});
|
||||||
|
const statusBorder =
|
||||||
|
check.status === "placeholder"
|
||||||
|
? theme.palette.divider
|
||||||
|
: check.status === true
|
||||||
|
? theme.palette.success.main
|
||||||
|
: theme.palette.error.main;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HeatmapResponseTimeTooltip
|
||||||
|
key={`${check}-${index}`}
|
||||||
|
check={check}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateRows: "auto auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
width: "100%",
|
||||||
|
aspectRatio: "10",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
bgcolor: statusBg,
|
||||||
|
borderRadius: theme.spacing(0.5),
|
||||||
|
...availabilityCellSx,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
pointerEvents: "none",
|
||||||
|
borderRadius: theme.spacing(0.5),
|
||||||
|
boxShadow: `inset 0 0 0 2px ${statusBorder}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
aspectRatio: "1 / 1",
|
||||||
|
bgcolor: respBg,
|
||||||
|
borderRadius: theme.spacing(0.5),
|
||||||
|
...responseCellSx,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</HeatmapResponseTimeTooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||||
|
import type { CheckSnapshot } from "@/Types/Check";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import type { RootState } from "@/Types/state";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
type HeatmapCheck =
|
||||||
|
| CheckSnapshot
|
||||||
|
| { status: "placeholder"; responseTime: 0; createdAt: "" };
|
||||||
|
|
||||||
|
export const HeatmapResponseTimeTooltip = ({
|
||||||
|
children,
|
||||||
|
check,
|
||||||
|
}: {
|
||||||
|
children: React.ReactElement;
|
||||||
|
check: HeatmapCheck;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const getColor = (status: boolean) => {
|
||||||
|
if (status === true) return theme.palette.success.light;
|
||||||
|
if (status === false) return theme.palette.error.light;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (check.status === "placeholder") {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
slotProps={{
|
||||||
|
tooltip: {
|
||||||
|
sx: {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
color: "inherit",
|
||||||
|
boxShadow: "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
<Stack
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.secondary.main,
|
||||||
|
border: 1,
|
||||||
|
borderColor: theme.palette.divider,
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
p: theme.spacing(4),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography>
|
||||||
|
{formatDateWithTz(check?.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)}
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
{t("common.labels.responseTime")}: {check.originalResponseTime.toFixed()} ms
|
||||||
|
</Typography>
|
||||||
|
<Typography textTransform={"capitalize"}>
|
||||||
|
Status:{" "}
|
||||||
|
<span style={{ color: getColor(check?.status) }}>
|
||||||
|
{check?.status === true
|
||||||
|
? t("pages.common.monitors.status.up")
|
||||||
|
: t("pages.common.monitors.status.down")}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import type { ResponsiveStyleValue } from "@mui/system";
|
||||||
|
import type { CheckSnapshot } from "@/Types/Check";
|
||||||
|
import { HeatmapResponseTimeTooltip } from "./HeatmapResponseTimeTooltip";
|
||||||
|
|
||||||
|
interface HistogramResponseTimeProps {
|
||||||
|
checks: CheckSnapshot[];
|
||||||
|
height?: ResponsiveStyleValue<number | string>;
|
||||||
|
gap?: ResponsiveStyleValue<number | string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_HEIGHT = 50;
|
||||||
|
|
||||||
|
export const HistogramResponseTime = ({
|
||||||
|
checks,
|
||||||
|
height = DEFAULT_HEIGHT,
|
||||||
|
gap,
|
||||||
|
}: HistogramResponseTimeProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
if (!Array.isArray(checks) || checks.length === 0) return null;
|
||||||
|
|
||||||
|
let data = Array<any>();
|
||||||
|
|
||||||
|
if (!checks || checks.length === 0) {
|
||||||
|
data = [];
|
||||||
|
}
|
||||||
|
if (checks.length !== 25) {
|
||||||
|
const placeholders = Array(25 - checks.length).fill({
|
||||||
|
status: "placeholder",
|
||||||
|
});
|
||||||
|
data = [...checks, ...placeholders];
|
||||||
|
} else {
|
||||||
|
data = checks;
|
||||||
|
}
|
||||||
|
const chartHeight = typeof height === "number" ? `${height}px` : height;
|
||||||
|
const gridGap = gap ?? theme.spacing(0.5);
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: "100%" }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(25, 1fr)",
|
||||||
|
gap: gridGap,
|
||||||
|
alignItems: "end",
|
||||||
|
height: chartHeight,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{data.map((check, index) => {
|
||||||
|
const isPlaceholder = (check as any).status === "placeholder";
|
||||||
|
const heightPct = `${Math.max(0, Math.min(100, (check as any).responseTime ?? 0))}%`;
|
||||||
|
const barColor =
|
||||||
|
check.status === true
|
||||||
|
? theme.palette.success.light
|
||||||
|
: theme.palette.error.light;
|
||||||
|
const bar = (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
borderRadius: theme.spacing(1),
|
||||||
|
bgcolor: theme.palette.action.hover,
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: isPlaceholder ? 0 : heightPct,
|
||||||
|
bgcolor: barColor,
|
||||||
|
transition: "height 500ms cubic-bezier(0.4, 0, 0.2, 1)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
return isPlaceholder ? (
|
||||||
|
<Box
|
||||||
|
key={index}
|
||||||
|
sx={{ height: "100%" }}
|
||||||
|
>
|
||||||
|
{bar}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<HeatmapResponseTimeTooltip
|
||||||
|
key={index}
|
||||||
|
check={check}
|
||||||
|
>
|
||||||
|
{bar}
|
||||||
|
</HeatmapResponseTimeTooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { Stack } from "@mui/material";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { ButtonInput } from "../../inputs/Button";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
interface HeaderCreateProps {
|
||||||
|
label?: string;
|
||||||
|
path: string;
|
||||||
|
isLoading?: boolean;
|
||||||
|
isAdmin?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HeaderCreate = ({
|
||||||
|
label,
|
||||||
|
path,
|
||||||
|
isLoading = false,
|
||||||
|
isAdmin = false,
|
||||||
|
}: HeaderCreateProps) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
navigate(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
justifyContent="flex-end"
|
||||||
|
alignItems="center"
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
<ButtonInput
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={handleClick}
|
||||||
|
loading={isLoading}
|
||||||
|
>
|
||||||
|
{label || t("common.buttons.create")}
|
||||||
|
</ButtonInput>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { ToggleButtonGroup, ToggleButton } from "@/Components/v2/inputs";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useMediaQuery } from "@mui/material";
|
||||||
|
|
||||||
|
interface MonitorTimeFrameHeaderProps {
|
||||||
|
isLoading?: boolean;
|
||||||
|
hasDateRange?: boolean;
|
||||||
|
dateRange: string;
|
||||||
|
setDateRange: (dateRange: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HeaderTimeRange = ({
|
||||||
|
isLoading = false,
|
||||||
|
hasDateRange = true,
|
||||||
|
dateRange,
|
||||||
|
setDateRange,
|
||||||
|
}: MonitorTimeFrameHeaderProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down("md"));
|
||||||
|
const handleChange = (
|
||||||
|
_event: React.MouseEvent<HTMLElement>,
|
||||||
|
newValue: string | null
|
||||||
|
) => {
|
||||||
|
if (newValue !== null) {
|
||||||
|
setDateRange(newValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let timeFramePicker = null;
|
||||||
|
|
||||||
|
if (hasDateRange) {
|
||||||
|
timeFramePicker = (
|
||||||
|
<ToggleButtonGroup
|
||||||
|
orientation={isSmallScreen ? "vertical" : "horizontal"}
|
||||||
|
value={dateRange}
|
||||||
|
exclusive
|
||||||
|
onChange={handleChange}
|
||||||
|
size="small"
|
||||||
|
fullWidth={isSmallScreen}
|
||||||
|
>
|
||||||
|
<ToggleButton value="recent">
|
||||||
|
{t("components.headerTimeRange.labels.recent")}
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="day">
|
||||||
|
{t("components.headerTimeRange.labels.day")}
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="week">
|
||||||
|
{t("components.headerTimeRange.labels.week")}
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="month">
|
||||||
|
{t("components.headerTimeRange.labels.month")}
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
direction={{ xs: "column", md: "row" }}
|
||||||
|
justifyContent="flex-end"
|
||||||
|
alignItems="center"
|
||||||
|
gap={theme.spacing(4)}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
>
|
||||||
|
{isLoading && <CircularProgress size={16} />}
|
||||||
|
</Box>
|
||||||
|
<Typography variant="body2">
|
||||||
|
{t(`components.headerTimeRange.description.${dateRange}`)}
|
||||||
|
</Typography>
|
||||||
|
{timeFramePicker}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export * from "../common/charts/HistogramResponseTime";
|
||||||
|
export * from "../common/charts/HeatmapResponseTime";
|
||||||
|
export * from "../common/charts/HeatmapResponseTimeTooltip";
|
||||||
|
export * from "../common/controls/HeaderCreate";
|
||||||
|
export * from "../common/controls/HeaderTimeRange";
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import Box, { type BoxProps } from "@mui/material/Box";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
export const BaseBox = ({ sx, ...rest }: BoxProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
{...rest}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
border: 1,
|
||||||
|
borderStyle: "solid",
|
||||||
|
borderColor: theme.palette.divider,
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
...sx,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { BaseBox } from "../design-elements";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import type { ResponsiveStyleValue } from "@mui/system";
|
||||||
|
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
type BaseChartProps = React.PropsWithChildren<{
|
||||||
|
icon: React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
width?: number | string;
|
||||||
|
maxWidth?: number | string;
|
||||||
|
padding?: number | string | ResponsiveStyleValue<number | string>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export const BaseChart = ({
|
||||||
|
children,
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
width = "100%",
|
||||||
|
maxWidth = "100%",
|
||||||
|
padding,
|
||||||
|
}: BaseChartProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<BaseBox
|
||||||
|
sx={{
|
||||||
|
padding: padding ?? theme.spacing(8),
|
||||||
|
display: "flex",
|
||||||
|
flex: 1,
|
||||||
|
width: width,
|
||||||
|
maxWidth: maxWidth,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
gap={theme.spacing(8)}
|
||||||
|
flex={1}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems={"center"}
|
||||||
|
gap={theme.spacing(4)}
|
||||||
|
>
|
||||||
|
{icon && (
|
||||||
|
<BaseBox
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: 34,
|
||||||
|
height: 34,
|
||||||
|
backgroundColor: theme.palette.action.hover,
|
||||||
|
"& svg": {
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
"& path": {
|
||||||
|
stroke: theme.palette.text.secondary,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</BaseBox>
|
||||||
|
)}
|
||||||
|
<Typography variant="h2">{title}</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Box flex={1}>{children}</Box>
|
||||||
|
</Stack>
|
||||||
|
</BaseBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
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 {
|
||||||
|
ErrorFallback,
|
||||||
|
EmptyFallback,
|
||||||
|
EmptyMonitorFallback,
|
||||||
|
BaseFallback,
|
||||||
|
} 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";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
import { Typography } from "@mui/material";
|
||||||
|
|
||||||
|
export const PageSpeedKeyPriorityFallback = () => {
|
||||||
|
return (
|
||||||
|
<BaseFallback>
|
||||||
|
<Alert
|
||||||
|
severity="warning"
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
maxWidth: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography>
|
||||||
|
<Trans
|
||||||
|
i18nKey="common.alerts.pageSpeedApiKey.content"
|
||||||
|
components={{
|
||||||
|
settingsLink: (
|
||||||
|
<Link
|
||||||
|
component={RouterLink}
|
||||||
|
to="/settings"
|
||||||
|
color="inherit"
|
||||||
|
fontWeight="inherit"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
</Alert>
|
||||||
|
</BaseFallback>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface BasePageProps extends StackProps {
|
||||||
|
loading?: boolean;
|
||||||
|
error?: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
breadcrumbOverride?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BasePage = ({
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
children,
|
||||||
|
breadcrumbOverride,
|
||||||
|
...props
|
||||||
|
}: BasePageProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
sx={{ height: "100%" }}
|
||||||
|
>
|
||||||
|
<CircularProgress
|
||||||
|
color="primary"
|
||||||
|
size={28}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<ErrorFallback
|
||||||
|
title="Something went wrong..."
|
||||||
|
subtitle="Please try again later"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
spacing={theme.spacing(10)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Breadcrumb breadcrumbOverride={breadcrumbOverride} />
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface BasePageWithStatesProps extends StackProps {
|
||||||
|
loading: boolean;
|
||||||
|
error: any;
|
||||||
|
totalCount: number;
|
||||||
|
bullets: string[] | unknown;
|
||||||
|
page: string;
|
||||||
|
actionButtonText: string;
|
||||||
|
actionLink: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BasePageWithStates = ({
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
totalCount,
|
||||||
|
page,
|
||||||
|
bullets,
|
||||||
|
actionButtonText,
|
||||||
|
actionLink,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: BasePageWithStatesProps) => {
|
||||||
|
const showLoading = loading && totalCount === 0;
|
||||||
|
|
||||||
|
if (!loading && totalCount === 0) {
|
||||||
|
return (
|
||||||
|
<EmptyFallback
|
||||||
|
bullets={bullets}
|
||||||
|
title={page}
|
||||||
|
actionButtonText={actionButtonText}
|
||||||
|
actionLink={actionLink}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BasePage
|
||||||
|
loading={showLoading}
|
||||||
|
error={error}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</BasePage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface MonitorBasePageWithStatesProps extends StackProps {
|
||||||
|
loading: boolean;
|
||||||
|
error: any;
|
||||||
|
totalCount: number;
|
||||||
|
page: string;
|
||||||
|
actionLink?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
priorityFallback?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MonitorBasePageWithStates = ({
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
totalCount,
|
||||||
|
page,
|
||||||
|
actionLink,
|
||||||
|
children,
|
||||||
|
priorityFallback,
|
||||||
|
...props
|
||||||
|
}: MonitorBasePageWithStatesProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const showLoading = loading && totalCount === 0;
|
||||||
|
|
||||||
|
if (priorityFallback) {
|
||||||
|
return (
|
||||||
|
<BasePage
|
||||||
|
loading={loading}
|
||||||
|
error={error}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{priorityFallback}
|
||||||
|
</BasePage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loading && totalCount === 0) {
|
||||||
|
return (
|
||||||
|
<EmptyMonitorFallback
|
||||||
|
page={page}
|
||||||
|
title={t(`pages.${page}.fallback.title`)}
|
||||||
|
bullets={t(`pages.${page}.fallback.checks`, { returnObjects: true })}
|
||||||
|
actionButtonText={t(`pages.${page}.fallback.actionButton`)}
|
||||||
|
actionLink={actionLink || ""}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BasePage
|
||||||
|
loading={showLoading}
|
||||||
|
error={error}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import MuiBreadcrumbs from "@mui/material/Breadcrumbs";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { Link, useLocation } from "react-router-dom";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { ChevronRight } from "lucide-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
const isId = (segment: string): boolean => {
|
||||||
|
return segment.length === 24 || /^[a-f0-9-]{36}$/.test(segment);
|
||||||
|
};
|
||||||
|
|
||||||
|
const actionSegments = ["create", "configure"];
|
||||||
|
|
||||||
|
const BreadcrumbWrapper = ({ children }: { children: ReactNode }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<MuiBreadcrumbs
|
||||||
|
separator={
|
||||||
|
<ChevronRight
|
||||||
|
size={16}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
marginBottom: theme.spacing(6),
|
||||||
|
"& .MuiBreadcrumbs-separator": {
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</MuiBreadcrumbs>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Breadcrumb = ({
|
||||||
|
breadcrumbOverride,
|
||||||
|
}: {
|
||||||
|
breadcrumbOverride?: string[] | undefined;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const theme = useTheme();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
// If override is an empty array, hide entirely
|
||||||
|
if (breadcrumbOverride !== undefined && breadcrumbOverride.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If override has items, render them directly
|
||||||
|
if (breadcrumbOverride !== undefined && breadcrumbOverride.length > 0) {
|
||||||
|
return (
|
||||||
|
<BreadcrumbWrapper>
|
||||||
|
{breadcrumbOverride.map((item, index) => {
|
||||||
|
const isLast = index === breadcrumbOverride.length - 1;
|
||||||
|
return (
|
||||||
|
<Typography
|
||||||
|
key={index}
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
fontWeight: isLast ? 600 : 400,
|
||||||
|
color: isLast ? theme.palette.primary.main : theme.palette.text.secondary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</BreadcrumbWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default behavior: use location pathname
|
||||||
|
const segments = location.pathname.split("/").filter((x) => x);
|
||||||
|
|
||||||
|
if (segments.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build simplified breadcrumb: "uptime" or "uptime / details" or "uptime / create"
|
||||||
|
const basePage = segments[0] || t("common.breadcrumbs.home");
|
||||||
|
|
||||||
|
const secondSegment = segments[1];
|
||||||
|
const isActionPage = secondSegment && actionSegments.includes(secondSegment);
|
||||||
|
const isDetailsPage = secondSegment && isId(secondSegment);
|
||||||
|
const hasSubPage = isActionPage || isDetailsPage;
|
||||||
|
|
||||||
|
const getSubPageLabel = (): string => {
|
||||||
|
if (isActionPage) {
|
||||||
|
return secondSegment.charAt(0).toUpperCase() + secondSegment.slice(1);
|
||||||
|
}
|
||||||
|
return t("common.breadcrumbs.details");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BreadcrumbWrapper>
|
||||||
|
{hasSubPage ? (
|
||||||
|
<Link
|
||||||
|
to={`/${basePage}`}
|
||||||
|
style={{ textDecoration: "none" }}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
"&:hover": {
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{basePage.charAt(0).toUpperCase() + basePage.slice(1)}
|
||||||
|
</Typography>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{basePage.charAt(0).toUpperCase() + basePage.slice(1)}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{hasSubPage && (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getSubPageLabel()}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</BreadcrumbWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { Check } from "lucide-react";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
export const BulletPointCheck = ({
|
||||||
|
text,
|
||||||
|
variant = "info",
|
||||||
|
}: {
|
||||||
|
text: string;
|
||||||
|
noHighlightText?: string;
|
||||||
|
variant?: "success" | "error" | "info";
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const colors: Record<string, string | undefined> = {
|
||||||
|
success: theme.palette.success.main,
|
||||||
|
error: theme.palette.error.main,
|
||||||
|
info: theme.palette.text.secondary,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
gap={theme.spacing(6)}
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
size={16}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
style={{
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography
|
||||||
|
component="span"
|
||||||
|
color={variant === "info" ? theme.palette.text.secondary : colors[variant]}
|
||||||
|
fontWeight={450}
|
||||||
|
sx={{
|
||||||
|
opacity: 0.9,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { CSSProperties } from "react";
|
||||||
|
|
||||||
|
interface DotProps {
|
||||||
|
color?: string;
|
||||||
|
size?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Dot = ({ color = "gray", size = "4px", style }: DotProps) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
content: '""',
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: color,
|
||||||
|
opacity: 0.8,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { BulletPointCheck, SkeletonCard } from "@/Components/v2/design-elements";
|
||||||
|
import { Button } from "@/Components/v2/inputs";
|
||||||
|
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import type { StackProps } from "@mui/material/Stack";
|
||||||
|
|
||||||
|
interface BaseFallbackProps extends StackProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BaseFallback = ({ children, ...props }: BaseFallbackProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
alignItems={"center"}
|
||||||
|
margin={isSmall ? "inherit" : "auto"}
|
||||||
|
marginTop={isSmall ? "33%" : "10%"}
|
||||||
|
width={{
|
||||||
|
sm: "90%",
|
||||||
|
md: "70%",
|
||||||
|
lg: "50%",
|
||||||
|
xl: "40%",
|
||||||
|
}}
|
||||||
|
padding={{ xs: theme.spacing(8), md: theme.spacing(16) }}
|
||||||
|
bgcolor={theme.palette.background.paper}
|
||||||
|
border={1}
|
||||||
|
borderColor={theme.palette.divider}
|
||||||
|
borderRadius={theme.shape.borderRadius}
|
||||||
|
sx={{
|
||||||
|
borderStyle: "dashed",
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SkeletonCard showHalo={true} />
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ErrorFallback = ({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<BaseFallback>
|
||||||
|
<Typography
|
||||||
|
variant="h1"
|
||||||
|
marginY={theme.spacing(4)}
|
||||||
|
color={theme.palette.text.secondary}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Typography>{subtitle}</Typography>
|
||||||
|
</BaseFallback>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmptyFallback = ({
|
||||||
|
title,
|
||||||
|
bullets,
|
||||||
|
actionButtonText,
|
||||||
|
actionLink,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
bullets: any;
|
||||||
|
actionButtonText: string;
|
||||||
|
actionLink: string;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<BaseFallback>
|
||||||
|
<Stack
|
||||||
|
gap={theme.spacing(10)}
|
||||||
|
zIndex={1}
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
component="h1"
|
||||||
|
color={theme.palette.primary.contrastText}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Stack
|
||||||
|
sx={{
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
maxWidth: "1100px",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Array.isArray(bullets) &&
|
||||||
|
bullets?.map((bullet: string) => (
|
||||||
|
<BulletPointCheck
|
||||||
|
text={bullet}
|
||||||
|
key={`${bullet}-${Math.random()}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => navigate(actionLink)}
|
||||||
|
>
|
||||||
|
{actionButtonText}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</BaseFallback>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmptyMonitorFallback = ({
|
||||||
|
page,
|
||||||
|
title,
|
||||||
|
bullets,
|
||||||
|
actionButtonText,
|
||||||
|
actionLink,
|
||||||
|
}: {
|
||||||
|
page: string;
|
||||||
|
title: string;
|
||||||
|
bullets: any;
|
||||||
|
actionButtonText: string;
|
||||||
|
actionLink: string;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<BaseFallback>
|
||||||
|
<Stack
|
||||||
|
gap={theme.spacing(10)}
|
||||||
|
zIndex={1}
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
component="h1"
|
||||||
|
color={theme.palette.primary.contrastText}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Stack
|
||||||
|
sx={{
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
maxWidth: "1100px",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Array.isArray(bullets) &&
|
||||||
|
bullets?.map((bullet: string, index: number) => (
|
||||||
|
<BulletPointCheck
|
||||||
|
text={bullet}
|
||||||
|
key={`${(page + "Monitors").trim().split(" ")[0]}-${index}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => navigate(actionLink)}
|
||||||
|
>
|
||||||
|
{actionButtonText}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</BaseFallback>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { useMemo, useState, useEffect } from "react";
|
||||||
|
import { getInfraGaugeColor } from "@/Utils/MonitorUtils";
|
||||||
|
|
||||||
|
const MINIMUM_VALUE = 0;
|
||||||
|
const MAXIMUM_VALUE = 100;
|
||||||
|
|
||||||
|
export const Gauge = ({
|
||||||
|
isLoading = false,
|
||||||
|
progress = 0,
|
||||||
|
radius = 70,
|
||||||
|
strokeWidth = 15,
|
||||||
|
precision = 1,
|
||||||
|
unit = "%",
|
||||||
|
}: {
|
||||||
|
isLoading?: boolean;
|
||||||
|
progress?: number;
|
||||||
|
radius?: number;
|
||||||
|
strokeWidth?: number;
|
||||||
|
precision?: number;
|
||||||
|
unit?: string;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const progressWithinRange = Math.max(MINIMUM_VALUE, Math.min(progress, MAXIMUM_VALUE));
|
||||||
|
|
||||||
|
// 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]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [offset, setOffset] = useState(circumference);
|
||||||
|
useEffect(() => {
|
||||||
|
setOffset(circumference);
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setOffset(circumference - strokeLength);
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [progress, circumference, strokeLength]);
|
||||||
|
|
||||||
|
const fillColor = getInfraGaugeColor(progressWithinRange, theme);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
display={"inline-block"}
|
||||||
|
position={"relative"}
|
||||||
|
width={radius}
|
||||||
|
height={radius}
|
||||||
|
bgcolor={theme.palette.background.paper}
|
||||||
|
borderRadius={"50%"}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox={`0 0 ${totalSize} ${totalSize}`}
|
||||||
|
width={radius}
|
||||||
|
height={radius}
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
stroke={theme.palette.secondary.main}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
fill="none"
|
||||||
|
cx={totalSize / 2}
|
||||||
|
cy={totalSize / 2}
|
||||||
|
r={radius}
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
stroke={fillColor}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeDasharray={`${circumference} ${circumference}`}
|
||||||
|
strokeDashoffset={offset}
|
||||||
|
fill="none"
|
||||||
|
cx={totalSize / 2}
|
||||||
|
cy={totalSize / 2}
|
||||||
|
r={radius}
|
||||||
|
style={{
|
||||||
|
transform: "rotate(-90deg)",
|
||||||
|
transformOrigin: "center",
|
||||||
|
transition: "stroke-dashoffset 1.5s ease-in-out",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{`${progressWithinRange.toFixed(precision)}${unit}`}
|
||||||
|
</Typography>
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
|
interface IconProps {
|
||||||
|
icon: LucideIcon;
|
||||||
|
size?: number;
|
||||||
|
strokeWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Icon = ({ icon: Icon, size = 20, strokeWidth = 1.5 }: IconProps) => {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
size={size}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Icon;
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import type { Monitor } from "@/Types/Monitor";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { PulseDot, Dot } from "@/Components/v2/design-elements";
|
||||||
|
import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import prettyMilliseconds from "pretty-ms";
|
||||||
|
import { typographyLevels } from "@/Utils/Theme/v2Palette";
|
||||||
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
|
export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||||
|
|
||||||
|
if (!monitor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<Typography
|
||||||
|
fontSize={typographyLevels.xl}
|
||||||
|
fontWeight={500}
|
||||||
|
color={theme.palette.primary.contrastText}
|
||||||
|
overflow={"hidden"}
|
||||||
|
textOverflow={"ellipsis"}
|
||||||
|
whiteSpace={"nowrap"}
|
||||||
|
maxWidth={isSmall ? "100%" : "calc((100vw - var(--env-var-width-2)) / 2)"}
|
||||||
|
>
|
||||||
|
{monitor.name}
|
||||||
|
</Typography>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems={"center"}
|
||||||
|
gap={theme.spacing(4)}
|
||||||
|
>
|
||||||
|
<PulseDot color={getStatusColor(monitor.status, theme)} />
|
||||||
|
<Typography
|
||||||
|
fontSize={typographyLevels.l}
|
||||||
|
fontWeight={"bolder"}
|
||||||
|
fontFamily={"monospace"}
|
||||||
|
overflow={"hidden"}
|
||||||
|
textOverflow={"ellipsis"}
|
||||||
|
whiteSpace={"nowrap"}
|
||||||
|
maxWidth={isSmall ? "100%" : "calc((100vw - var(--env-var-width-2)) / 2)"}
|
||||||
|
>
|
||||||
|
{formatUrl(monitor?.url)}
|
||||||
|
</Typography>
|
||||||
|
{!isSmall && (
|
||||||
|
<>
|
||||||
|
<Dot />
|
||||||
|
<Typography>
|
||||||
|
Checking every {prettyMilliseconds(monitor?.interval, { verbose: true })}
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Box, Stack, useTheme } from "@mui/material";
|
||||||
|
|
||||||
|
interface PulseDotProps {
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PulseDot = ({ color }: PulseDotProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
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.background.paper,
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import { Box, Stack, useTheme } from "@mui/material";
|
||||||
|
|
||||||
|
interface SkeletonCardProps {
|
||||||
|
width?: number | string | { xs?: number | string; md?: number | string };
|
||||||
|
showHalo?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SkeletonCard = ({
|
||||||
|
width = { xs: 130, md: 216 },
|
||||||
|
showHalo = true,
|
||||||
|
}: SkeletonCardProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const pulseAnimation = {
|
||||||
|
"@keyframes pulse": {
|
||||||
|
"0%, 100%": { opacity: 1 },
|
||||||
|
"50%": { opacity: 0.72 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const floatAnimation = {
|
||||||
|
"@keyframes float": {
|
||||||
|
"0%, 100%": { transform: "rotate(5deg) translate(12px, -20px)" },
|
||||||
|
"50%": { transform: "rotate(5deg) translate(12px, -24px)" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const cardStyle = {
|
||||||
|
width: width,
|
||||||
|
height: 57,
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
background:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? theme.palette.secondary.dark
|
||||||
|
: theme.palette.background.paper,
|
||||||
|
boxShadow:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "0 8px 30px rgba(0,0,0,.3), 0 0 0 1px rgba(255,255,255,.05) inset"
|
||||||
|
: "0 8px 30px rgba(0,0,0,.08), 0 0 0 1px rgba(0,0,0,.05) inset",
|
||||||
|
display: "flex",
|
||||||
|
gap: theme.spacing(5),
|
||||||
|
alignItems: "center",
|
||||||
|
padding: theme.spacing(5),
|
||||||
|
};
|
||||||
|
|
||||||
|
const blockStyle = {
|
||||||
|
width: 55,
|
||||||
|
height: 34,
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
background: theme.palette.secondary.main,
|
||||||
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
|
animation: "pulse 1.6s ease-in-out infinite",
|
||||||
|
...pulseAnimation,
|
||||||
|
};
|
||||||
|
|
||||||
|
const lineStyle = {
|
||||||
|
height: 7,
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
background: theme.palette.secondary.main,
|
||||||
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
|
animation: "pulse 1.6s ease-in-out infinite",
|
||||||
|
...pulseAnimation,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
position={"relative"}
|
||||||
|
mt={20}
|
||||||
|
mb={20}
|
||||||
|
>
|
||||||
|
{showHalo && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
display: "grid",
|
||||||
|
placeItems: "center",
|
||||||
|
pointerEvents: "none",
|
||||||
|
"&::before": {
|
||||||
|
content: '""',
|
||||||
|
width: "260px",
|
||||||
|
height: "260px",
|
||||||
|
borderRadius: "999px",
|
||||||
|
background:
|
||||||
|
theme.palette.mode === "dark" ? "rgba(255, 255, 255, 0.05)" : "#fff",
|
||||||
|
filter: "blur(60px)",
|
||||||
|
opacity: 0.9,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Card stack */}
|
||||||
|
<Box sx={{ position: "relative", zIndex: 1 }}>
|
||||||
|
{/* Front card (now at bottom) */}
|
||||||
|
<Box sx={cardStyle}>
|
||||||
|
<Box sx={blockStyle} />
|
||||||
|
<Stack sx={{ flex: 1, gap: "12px" }}>
|
||||||
|
{/* Empty space to maintain card height */}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Back card A */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
...cardStyle,
|
||||||
|
transform: "rotate(-4deg) translate(-12px, -12px)",
|
||||||
|
filter: "blur(.2px)",
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<Box sx={blockStyle} />
|
||||||
|
<Stack sx={{ flex: 1, gap: "12px" }}>
|
||||||
|
<Box sx={{ ...lineStyle, width: "60%" }} />
|
||||||
|
<Box sx={{ ...lineStyle, width: "80%" }} />
|
||||||
|
<Box sx={{ ...lineStyle, width: "40%" }} />
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Back card B */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
...cardStyle,
|
||||||
|
transform: "rotate(5deg) translate(12px, -20px)",
|
||||||
|
filter: "blur(.3px)",
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
animation: "float 3s ease-in-out 5",
|
||||||
|
...floatAnimation,
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<Box sx={blockStyle} />
|
||||||
|
<Stack sx={{ flex: 1, gap: "12px" }}>
|
||||||
|
<Box sx={{ ...lineStyle, width: "60%" }} />
|
||||||
|
<Box sx={{ ...lineStyle, width: "80%" }} />
|
||||||
|
<Box sx={{ ...lineStyle, width: "40%" }} />
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
|
export const SplitBox = ({
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
}: {
|
||||||
|
left: React.ReactNode;
|
||||||
|
right: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
direction={isSmall ? "column" : "row"}
|
||||||
|
bgcolor={theme.palette.background.paper}
|
||||||
|
border={1}
|
||||||
|
borderColor={theme.palette.divider}
|
||||||
|
borderRadius={theme.spacing(2)}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
padding={theme.spacing(15)}
|
||||||
|
borderRight={isSmall ? 0 : 1}
|
||||||
|
borderBottom={isSmall ? 1 : 0}
|
||||||
|
borderColor={theme.palette.divider}
|
||||||
|
flex={0.7}
|
||||||
|
sx={{
|
||||||
|
background:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "linear-gradient(135deg, rgba(255, 255, 255, 0.01) 0%, rgba(255, 255, 255, 0.02) 100%)"
|
||||||
|
: "linear-gradient(135deg, rgba(0, 0, 0, 0.01) 0%, rgba(0, 0, 0, 0.02) 100%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{left}
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
flex={1}
|
||||||
|
padding={theme.spacing(15)}
|
||||||
|
>
|
||||||
|
{right}
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConfigBox = ({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
leftContent,
|
||||||
|
rightContent,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
leftContent?: React.ReactNode;
|
||||||
|
rightContent: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<SplitBox
|
||||||
|
left={
|
||||||
|
<Stack spacing={theme.spacing(4)}>
|
||||||
|
<Typography
|
||||||
|
textTransform={"capitalize"}
|
||||||
|
component="h2"
|
||||||
|
variant="h2"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Typography component="p">{subtitle}</Typography>
|
||||||
|
{leftContent}
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
right={rightContent}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { lighten } from "@mui/material/styles";
|
||||||
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
|
import type { PaletteKey } from "@/Utils/Theme/v2Theme";
|
||||||
|
import { BaseBox, TooltipWithInfo } from "@/Components/v2/design-elements";
|
||||||
|
import type { SxProps } from "@mui/material";
|
||||||
|
|
||||||
|
type GradientBox = React.PropsWithChildren<{
|
||||||
|
palette?: PaletteKey;
|
||||||
|
sx?: SxProps;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export const GradientBox = ({ children, palette, sx }: GradientBox) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||||
|
const isLight = theme.palette.mode === "light";
|
||||||
|
const paper = theme.palette.background.paper;
|
||||||
|
const paperStart = lighten(paper, isLight ? 0.06 : 0.08);
|
||||||
|
const paperEnd = paper;
|
||||||
|
const bg = palette
|
||||||
|
? `linear-gradient(135deg, ${theme.palette[palette].light} 0%, ${theme.palette[palette].main} 100%)`
|
||||||
|
: `linear-gradient(135deg, ${paperStart} 0%, ${paperEnd} 100%)`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseBox
|
||||||
|
sx={{
|
||||||
|
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
|
||||||
|
width: isSmall ? `100%` : `calc(25% - (3 * ${theme.spacing(8)} / 4))`,
|
||||||
|
background: bg,
|
||||||
|
...sx,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</BaseBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type StatBoxProps = React.PropsWithChildren<{
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
palette?: PaletteKey;
|
||||||
|
sx?: SxProps;
|
||||||
|
tooltip?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export const StatBox = ({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
palette,
|
||||||
|
children,
|
||||||
|
sx,
|
||||||
|
tooltip,
|
||||||
|
onClick,
|
||||||
|
}: StatBoxProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const textColor = palette ? theme.palette[palette].contrastText : "inherit";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GradientBox
|
||||||
|
palette={palette}
|
||||||
|
sx={{
|
||||||
|
...(sx as object),
|
||||||
|
...(onClick ? { cursor: "pointer", "&:hover": { opacity: 0.95 } } : {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack onClick={onClick}>
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", gap: theme.spacing(2) }}>
|
||||||
|
<Typography color={textColor}>{title}</Typography>
|
||||||
|
{tooltip && (
|
||||||
|
<TooltipWithInfo
|
||||||
|
title={tooltip}
|
||||||
|
iconColor={textColor as string}
|
||||||
|
iconSize={14}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Typography color={textColor}>{subtitle}</Typography>
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
</GradientBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import { BaseBox } from "@/Components/v2/design-elements";
|
||||||
|
import Background from "@/assets/Images/background-grid.svg?react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import type { SxProps } from "@mui/material";
|
||||||
|
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
type StatusBoxProps = React.PropsWithChildren<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
sx?: SxProps;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export const BGBox = ({ children, sx }: StatusBoxProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<BaseBox
|
||||||
|
sx={{
|
||||||
|
overflow: "hidden",
|
||||||
|
position: "relative",
|
||||||
|
flex: 1,
|
||||||
|
padding: theme.spacing(4),
|
||||||
|
...sx,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top="-10%"
|
||||||
|
left="5%"
|
||||||
|
>
|
||||||
|
<Background />
|
||||||
|
</Box>
|
||||||
|
{children}
|
||||||
|
</BaseBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const StatusBox = ({
|
||||||
|
label,
|
||||||
|
n,
|
||||||
|
color,
|
||||||
|
sx,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
n: number;
|
||||||
|
color: string | undefined;
|
||||||
|
sx?: SxProps;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<BGBox sx={sx}>
|
||||||
|
<Stack spacing={theme.spacing(4)}>
|
||||||
|
<Typography
|
||||||
|
variant={"h2"}
|
||||||
|
textTransform="uppercase"
|
||||||
|
color={theme.palette.text.secondary}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h1"
|
||||||
|
color={color}
|
||||||
|
>
|
||||||
|
{n}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</BGBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UpStatusBox = ({ n }: { n: number }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<StatusBox
|
||||||
|
label={t("pages.common.monitors.status.up")}
|
||||||
|
n={n}
|
||||||
|
color={theme.palette.success.light}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DownStatusBox = ({ n }: { n: number }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<StatusBox
|
||||||
|
label={t("pages.common.monitors.status.down")}
|
||||||
|
n={n}
|
||||||
|
color={theme.palette.error.light}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PausedStatusBox = ({ n }: { n: number }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<StatusBox
|
||||||
|
label={t("pages.common.monitors.status.paused")}
|
||||||
|
n={n}
|
||||||
|
color={theme.palette.warning.light}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const TotalChecksBox = ({ n }: { n: number }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<StatusBox
|
||||||
|
label={t("pages.common.monitors.status.total")}
|
||||||
|
n={n}
|
||||||
|
color={theme.palette.primary.light}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const DownChecksBox = ({ n }: { n: number }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<StatusBox
|
||||||
|
label={t("pages.common.monitors.status.down")}
|
||||||
|
n={n}
|
||||||
|
color={theme.palette.error.light}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const UpChecksBox = ({ n }: { n: number }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<StatusBox
|
||||||
|
label={t("pages.common.monitors.status.up")}
|
||||||
|
n={n}
|
||||||
|
color={theme.palette.success.light}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InitializingStatusBox = ({ n }: { n: number }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<StatusBox
|
||||||
|
label={t("pages.common.monitors.status.initializing")}
|
||||||
|
n={n}
|
||||||
|
color={theme.palette.warning.light}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import { BaseBox } from "@/Components/v2/design-elements";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
|
import type { MonitorStatus } from "@/Types/Monitor";
|
||||||
|
import type { SxProps } from "@mui/material/styles";
|
||||||
|
import { getStatusPalette, getValuePalette } from "@/Utils/MonitorUtils";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export const ValueTypes = ["positive", "negative", "neutral"] as const;
|
||||||
|
export type ValueType = (typeof ValueTypes)[number];
|
||||||
|
|
||||||
|
export const StatusLabel = ({
|
||||||
|
status,
|
||||||
|
isActive,
|
||||||
|
sx,
|
||||||
|
}: {
|
||||||
|
status: MonitorStatus;
|
||||||
|
isActive?: boolean;
|
||||||
|
sx?: SxProps;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const theme = useTheme();
|
||||||
|
const palette = getStatusPalette(status);
|
||||||
|
|
||||||
|
const determineStatus = (
|
||||||
|
isActive: boolean | undefined,
|
||||||
|
status: MonitorStatus
|
||||||
|
): string => {
|
||||||
|
if (isActive === false) {
|
||||||
|
return t("pages.common.monitors.status.paused");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === true) {
|
||||||
|
return t("pages.common.monitors.status.up");
|
||||||
|
}
|
||||||
|
if (status === false) {
|
||||||
|
return t("pages.common.monitors.status.down");
|
||||||
|
}
|
||||||
|
return t("pages.common.monitors.status.initializing");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseBox
|
||||||
|
sx={{
|
||||||
|
display: "inline-flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
padding: theme.spacing(3, 5),
|
||||||
|
color: theme.palette[palette].main,
|
||||||
|
borderColor:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "rgba(255, 255, 255, 0.08)"
|
||||||
|
: "rgba(0, 0, 0, 0.08)",
|
||||||
|
...sx,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
width={7}
|
||||||
|
height={7}
|
||||||
|
bgcolor={theme.palette[palette].light}
|
||||||
|
borderRadius="50%"
|
||||||
|
marginRight="5px"
|
||||||
|
/>
|
||||||
|
<Typography textTransform={"capitalize"}>
|
||||||
|
{determineStatus(isActive, status)}
|
||||||
|
</Typography>
|
||||||
|
</BaseBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ValueLabel = ({ value, text }: { value: ValueType; text: string }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const palette = getValuePalette(value);
|
||||||
|
const transformedText = text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseBox
|
||||||
|
sx={{
|
||||||
|
display: "inline-flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
padding: theme.spacing(3, 5),
|
||||||
|
color: theme.palette[palette].main,
|
||||||
|
borderColor:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "rgba(255, 255, 255, 0.08)"
|
||||||
|
: "rgba(0, 0, 0, 0.08)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
width={7}
|
||||||
|
height={7}
|
||||||
|
bgcolor={theme.palette[palette].light}
|
||||||
|
borderRadius="50%"
|
||||||
|
marginRight="5px"
|
||||||
|
/>
|
||||||
|
<Typography>{transformedText}</Typography>
|
||||||
|
</BaseBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,595 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Grid2 from "@mui/material/Grid";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Paper from "@mui/material/Paper";
|
||||||
|
import Table from "@mui/material/Table";
|
||||||
|
import TableBody from "@mui/material/TableBody";
|
||||||
|
import TableCell from "@mui/material/TableCell";
|
||||||
|
import TableContainer from "@mui/material/TableContainer";
|
||||||
|
import TableHead from "@mui/material/TableHead";
|
||||||
|
import TableRow from "@mui/material/TableRow";
|
||||||
|
import Collapse from "@mui/material/Collapse";
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import {
|
||||||
|
ChevronsLeft,
|
||||||
|
ChevronsRight,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
Coffee,
|
||||||
|
Ellipsis,
|
||||||
|
PartyPopper,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
import TablePagination from "@mui/material/TablePagination";
|
||||||
|
import type { TablePaginationOwnProps } from "@mui/material/TablePagination";
|
||||||
|
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useState, Fragment, useEffect } from "react";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import type { SxProps, Theme } from "@mui/material/styles";
|
||||||
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
|
|
||||||
|
export type Header<T> = {
|
||||||
|
id: number | string;
|
||||||
|
content: React.ReactNode;
|
||||||
|
onClick?: (event: React.MouseEvent<HTMLTableCellElement | null>, row: T) => void;
|
||||||
|
render: (row: T) => React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DataTableProps<T extends { id?: string | number; _id?: string | number }> = {
|
||||||
|
headers: Header<T>[];
|
||||||
|
data: T[];
|
||||||
|
onRowClick?: (row: T) => void;
|
||||||
|
cardsOnSmallScreens?: boolean;
|
||||||
|
expandableRows?: boolean;
|
||||||
|
renderExpandedContent?: (row: T) => React.ReactNode;
|
||||||
|
emptyViewText?: string;
|
||||||
|
emptyViewPositive?: boolean;
|
||||||
|
getRowSx?: (row: T) => SxProps<Theme>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DataTable<
|
||||||
|
T extends {
|
||||||
|
id?: string | number;
|
||||||
|
_id?: string | number;
|
||||||
|
onRowClick?: (row: T) => void;
|
||||||
|
},
|
||||||
|
>({
|
||||||
|
headers,
|
||||||
|
data,
|
||||||
|
onRowClick,
|
||||||
|
cardsOnSmallScreens = true,
|
||||||
|
expandableRows = false,
|
||||||
|
renderExpandedContent,
|
||||||
|
emptyViewText,
|
||||||
|
emptyViewPositive,
|
||||||
|
getRowSx,
|
||||||
|
}: DataTableProps<T>) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [expanded, setExpanded] = useState<(string | number) | null>(null);
|
||||||
|
const handleExpand = (row: T) => {
|
||||||
|
const key = row.id || row._id || null;
|
||||||
|
setExpanded(expanded === key ? null : key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||||
|
|
||||||
|
if (data.length === 0 || headers.length === 0) {
|
||||||
|
return (
|
||||||
|
<EmptyView
|
||||||
|
text={emptyViewText}
|
||||||
|
positive={emptyViewPositive}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = [];
|
||||||
|
// Return stack of cards for small screens
|
||||||
|
if (isSmall && cardsOnSmallScreens) {
|
||||||
|
return (
|
||||||
|
<Stack spacing={theme.spacing(4)}>
|
||||||
|
{data.map((row) => {
|
||||||
|
const key = row.id || row._id || Math.random();
|
||||||
|
keys.push(key);
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
onClick={() => (onRowClick ? onRowClick(row) : null)}
|
||||||
|
spacing={theme.spacing(4)}
|
||||||
|
sx={{
|
||||||
|
borderStyle: "solid",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: theme.palette.divider,
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
padding: theme.spacing(4),
|
||||||
|
cursor: onRowClick ? "pointer" : "default",
|
||||||
|
}}
|
||||||
|
key={key}
|
||||||
|
>
|
||||||
|
{headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<Grid2
|
||||||
|
container
|
||||||
|
key={header.id}
|
||||||
|
>
|
||||||
|
<Grid2
|
||||||
|
size={5}
|
||||||
|
display={"flex"}
|
||||||
|
alignItems={"center"}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
component="div"
|
||||||
|
color={theme.palette.text.primary}
|
||||||
|
>
|
||||||
|
{header.content}
|
||||||
|
</Typography>
|
||||||
|
</Grid2>
|
||||||
|
<Grid2
|
||||||
|
size={7}
|
||||||
|
display="flex"
|
||||||
|
alignItems={"center"}
|
||||||
|
>
|
||||||
|
{header.render(row)}{" "}
|
||||||
|
</Grid2>
|
||||||
|
</Grid2>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{expandableRows && renderExpandedContent && renderExpandedContent(row) && (
|
||||||
|
<Grid2 size={12}>{renderExpandedContent(row)}</Grid2>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableContainer
|
||||||
|
component={Paper}
|
||||||
|
elevation={0}
|
||||||
|
sx={{ boxShadow: "none", overflowX: "hidden" }}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
sx={{
|
||||||
|
"&.MuiTable-root :is(.MuiTableHead-root, .MuiTableBody-root) :is(th, td)": {
|
||||||
|
paddingLeft: theme.spacing(8),
|
||||||
|
},
|
||||||
|
"& .MuiTableCell-root": {
|
||||||
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
|
},
|
||||||
|
"& .MuiTableHead-root .MuiTableRow-root": {
|
||||||
|
height: "28px",
|
||||||
|
},
|
||||||
|
"& :is(th)": {
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
fontWeight: 500,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
padding: `${theme.spacing(2)} ${theme.spacing(8)}`,
|
||||||
|
fontSize: theme.typography.fontSize,
|
||||||
|
},
|
||||||
|
"& :is(td)": {
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
padding: `${theme.spacing(7)} ${theme.spacing(8)}`,
|
||||||
|
fontSize: theme.typography.fontSize,
|
||||||
|
overflowX: "hidden",
|
||||||
|
},
|
||||||
|
"& .MuiTableBody-root .MuiTableRow-root:last-child .MuiTableCell-root": {
|
||||||
|
borderBottom: "none",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
{headers.map((header, idx) => {
|
||||||
|
return (
|
||||||
|
<TableCell
|
||||||
|
align={idx === 0 ? "left" : "center"}
|
||||||
|
key={header.id}
|
||||||
|
>
|
||||||
|
{header.content}
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{data.map((row) => {
|
||||||
|
const key = row.id || row._id || Math.random();
|
||||||
|
const isExpanded = expanded === key;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment key={key}>
|
||||||
|
<TableRow
|
||||||
|
sx={{
|
||||||
|
cursor: onRowClick ? "pointer" : "default",
|
||||||
|
...(getRowSx?.(row) as object),
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (expandableRows) handleExpand(row);
|
||||||
|
else if (onRowClick) onRowClick(row);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{headers.map((header, index) => {
|
||||||
|
return (
|
||||||
|
<TableCell
|
||||||
|
align={index === 0 ? "left" : "center"}
|
||||||
|
key={header.id}
|
||||||
|
onClick={
|
||||||
|
header.onClick ? (e) => header.onClick!(e, row) : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{header.render(row)}
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
{expandableRows &&
|
||||||
|
renderExpandedContent &&
|
||||||
|
renderExpandedContent(row) && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={headers.length}
|
||||||
|
style={{
|
||||||
|
borderBottom: isExpanded ? undefined : "none",
|
||||||
|
paddingTop: 0,
|
||||||
|
paddingBottom: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Collapse
|
||||||
|
in={isExpanded}
|
||||||
|
timeout="auto"
|
||||||
|
unmountOnExit
|
||||||
|
>
|
||||||
|
<Box sx={{ pt: 4, pb: 4 }}>{renderExpandedContent(row)}</Box>
|
||||||
|
</Collapse>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TablePaginationActionsProps {
|
||||||
|
count: number;
|
||||||
|
page: number;
|
||||||
|
rowsPerPage: number;
|
||||||
|
onPageChange: (event: React.MouseEvent<HTMLButtonElement>, newPage: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TablePaginationActions(props: TablePaginationActionsProps) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { count, page, rowsPerPage, onPageChange } = props;
|
||||||
|
|
||||||
|
const handleFirstPageButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
onPageChange(event, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBackButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
onPageChange(event, page - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNextButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
onPageChange(event, page + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLastPageButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{ flexShrink: 0, ml: { xs: 0, md: 2.5 } }}
|
||||||
|
className="table-pagination-actions"
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleFirstPageButtonClick}
|
||||||
|
disabled={page === 0}
|
||||||
|
aria-label="first page"
|
||||||
|
>
|
||||||
|
{theme.direction === "rtl" ? (
|
||||||
|
<ChevronsRight
|
||||||
|
size={20}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ChevronsLeft
|
||||||
|
size={20}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleBackButtonClick}
|
||||||
|
disabled={page === 0}
|
||||||
|
aria-label="previous page"
|
||||||
|
>
|
||||||
|
{theme.direction === "rtl" ? (
|
||||||
|
<ChevronRight
|
||||||
|
size={20}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ChevronLeft
|
||||||
|
size={20}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleNextButtonClick}
|
||||||
|
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||||
|
aria-label="next page"
|
||||||
|
>
|
||||||
|
{theme.direction === "rtl" ? (
|
||||||
|
<ChevronLeft
|
||||||
|
size={20}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ChevronRight
|
||||||
|
size={20}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleLastPageButtonClick}
|
||||||
|
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||||
|
aria-label="last page"
|
||||||
|
>
|
||||||
|
{theme.direction === "rtl" ? (
|
||||||
|
<ChevronsLeft
|
||||||
|
size={20}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ChevronsRight
|
||||||
|
size={20}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HasMoreTablePaginationActionsProps {
|
||||||
|
hasMore?: boolean;
|
||||||
|
page: number;
|
||||||
|
onPageChange: (event: React.MouseEvent<HTMLButtonElement>, newPage: number) => void;
|
||||||
|
}
|
||||||
|
function HasMoreTablePaginationActions(props: HasMoreTablePaginationActionsProps) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { hasMore, page, onPageChange } = props;
|
||||||
|
|
||||||
|
const handleFirstPageButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
onPageChange(event, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBackButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
onPageChange(event, page - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNextButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
onPageChange(event, page + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{ flexShrink: 0, ml: { xs: 0, md: 2.5 } }}
|
||||||
|
className="table-pagination-actions"
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleFirstPageButtonClick}
|
||||||
|
disabled={page === 0}
|
||||||
|
aria-label="first page"
|
||||||
|
>
|
||||||
|
{theme.direction === "rtl" ? (
|
||||||
|
<ChevronsRight
|
||||||
|
size={20}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ChevronsLeft
|
||||||
|
size={20}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleBackButtonClick}
|
||||||
|
disabled={page === 0}
|
||||||
|
aria-label="previous page"
|
||||||
|
>
|
||||||
|
{theme.direction === "rtl" ? (
|
||||||
|
<ChevronRight
|
||||||
|
size={20}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ChevronLeft
|
||||||
|
size={20}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleNextButtonClick}
|
||||||
|
disabled={hasMore === false}
|
||||||
|
aria-label="next page"
|
||||||
|
>
|
||||||
|
{theme.direction === "rtl" ? (
|
||||||
|
<ChevronLeft
|
||||||
|
size={20}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ChevronRight
|
||||||
|
size={20}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
<Tooltip title={hasMore === true ? "More pages available" : "No more pages"}>
|
||||||
|
<IconButton
|
||||||
|
disabled={hasMore === false}
|
||||||
|
aria-label="next page"
|
||||||
|
>
|
||||||
|
<Ellipsis
|
||||||
|
size={20}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PaginationProps extends TablePaginationOwnProps {
|
||||||
|
component?: React.ElementType;
|
||||||
|
hasMore?: boolean;
|
||||||
|
itemsOnPage?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Pagination = ({ ...props }: PaginationProps) => {
|
||||||
|
const { hasMore, itemsOnPage, ...rest } = props;
|
||||||
|
const isSmall = useMediaQuery((theme: any) => theme.breakpoints.down("sm"));
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
typeof itemsOnPage === "number" &&
|
||||||
|
itemsOnPage === 0 &&
|
||||||
|
rest.count > 0 &&
|
||||||
|
rest.page > 0 &&
|
||||||
|
rest.onPageChange
|
||||||
|
) {
|
||||||
|
rest.onPageChange(null, rest.page - 1);
|
||||||
|
}
|
||||||
|
}, [itemsOnPage, rest.count, rest.page, rest.onPageChange]);
|
||||||
|
|
||||||
|
const labelDisplayedRows = ({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
count,
|
||||||
|
}: {
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
count: number;
|
||||||
|
}) => {
|
||||||
|
if (hasMore) {
|
||||||
|
return to === 0 ? "" : `${from}–${to}`;
|
||||||
|
}
|
||||||
|
return `${from}–${to} of ${count}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TablePagination
|
||||||
|
ActionsComponent={(props) =>
|
||||||
|
typeof hasMore === "boolean" ? (
|
||||||
|
<HasMoreTablePaginationActions
|
||||||
|
{...props}
|
||||||
|
hasMore={hasMore}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TablePaginationActions {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
rowsPerPageOptions={[5, 10, 25]}
|
||||||
|
labelDisplayedRows={labelDisplayedRows}
|
||||||
|
{...rest}
|
||||||
|
sx={{
|
||||||
|
"& .MuiTablePagination-toolbar": isSmall
|
||||||
|
? {
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr 1fr",
|
||||||
|
gridAutoRows: "auto",
|
||||||
|
rowGap: theme.spacing(1),
|
||||||
|
alignItems: "center",
|
||||||
|
justifyItems: "center",
|
||||||
|
paddingLeft: 0,
|
||||||
|
paddingRight: 0,
|
||||||
|
}
|
||||||
|
: { display: "flex", alignItems: "center" },
|
||||||
|
"& .MuiTablePagination-selectLabel": {
|
||||||
|
gridColumn: "1",
|
||||||
|
gridRow: "1",
|
||||||
|
justifySelf: "center",
|
||||||
|
},
|
||||||
|
"& .MuiTablePagination-select": {
|
||||||
|
gridColumn: "2",
|
||||||
|
gridRow: "1",
|
||||||
|
justifySelf: "center",
|
||||||
|
},
|
||||||
|
"& .MuiTablePagination-displayedRows": isSmall
|
||||||
|
? { gridColumn: "1 / span 2", gridRow: "2", justifySelf: "center" }
|
||||||
|
: {},
|
||||||
|
"& .table-pagination-actions": isSmall
|
||||||
|
? { gridColumn: "1 / span 2", gridRow: "3", justifySelf: "center" }
|
||||||
|
: {},
|
||||||
|
"& .MuiSelect-select": {
|
||||||
|
border: 1,
|
||||||
|
borderColor: theme.palette.divider,
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const EmptyView = ({ text, positive }: { text?: string; positive?: boolean }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const Icon = positive ? PartyPopper : Coffee;
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
alignItems={"center"}
|
||||||
|
justifyContent={"center"}
|
||||||
|
sx={{
|
||||||
|
py: theme.spacing(12),
|
||||||
|
px: theme.spacing(4),
|
||||||
|
borderWidth: 1,
|
||||||
|
borderStyle: "solid",
|
||||||
|
borderColor: theme.palette.divider,
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: theme.palette.action.hover,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
mb: theme.spacing(2),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
size={28}
|
||||||
|
strokeWidth={1}
|
||||||
|
color={theme.palette.text.secondary}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
color={theme.palette.text.primary}
|
||||||
|
sx={{ fontWeight: 500 }}
|
||||||
|
>
|
||||||
|
{text ?? t("common.table.empty")}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import MuiTabs from "@mui/material/Tabs";
|
||||||
|
import type { TabsProps } from "@mui/material/Tabs";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
interface CustomTabsProps extends TabsProps {}
|
||||||
|
|
||||||
|
export const Tabs = (props: CustomTabsProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<MuiTabs
|
||||||
|
sx={{
|
||||||
|
minHeight: 34,
|
||||||
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
|
"& .MuiTabs-indicator": {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
height: 2,
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
"& .MuiTabs-flexContainer": {
|
||||||
|
gap: theme.spacing(16),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</MuiTabs>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
import MuiTab from "@mui/material/Tab";
|
||||||
|
import type { TabProps } from "@mui/material/Tab";
|
||||||
|
interface CustomTabProps extends TabProps {}
|
||||||
|
|
||||||
|
export const Tab = (props: CustomTabProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<MuiTab
|
||||||
|
disableRipple
|
||||||
|
iconPosition="start"
|
||||||
|
sx={{
|
||||||
|
textTransform: "none",
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 500,
|
||||||
|
minHeight: 34,
|
||||||
|
padding: theme.spacing(1, 0),
|
||||||
|
paddingBottom: 0,
|
||||||
|
minWidth: "auto",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
"&.Mui-selected": {
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
"&:hover": {
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
},
|
||||||
|
"& .MuiTab-iconWrapper": {
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
|
marginBottom: 0,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import type { StackProps } from "@mui/material/Stack";
|
||||||
|
import Link from "@mui/material/Link";
|
||||||
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
interface TextLinkProps extends StackProps {
|
||||||
|
text: string;
|
||||||
|
linkText: string;
|
||||||
|
href: string;
|
||||||
|
target?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextLink = ({
|
||||||
|
text,
|
||||||
|
linkText,
|
||||||
|
href,
|
||||||
|
target = "_self",
|
||||||
|
...props
|
||||||
|
}: TextLinkProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
gap={theme.spacing(4)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Typography>{text}</Typography>
|
||||||
|
<Link
|
||||||
|
color="primary"
|
||||||
|
to={href}
|
||||||
|
component={RouterLink}
|
||||||
|
target={target}
|
||||||
|
>
|
||||||
|
{linkText}
|
||||||
|
</Link>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import MuiTooltip from "@mui/material/Tooltip";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { Info } from "lucide-react";
|
||||||
|
|
||||||
|
import type { TooltipProps } from "@mui/material/Tooltip";
|
||||||
|
|
||||||
|
type StyledTooltipProps = TooltipProps;
|
||||||
|
|
||||||
|
export const Tooltip = ({ placement = "top", ...props }: StyledTooltipProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MuiTooltip
|
||||||
|
title={props.title}
|
||||||
|
placement={placement}
|
||||||
|
slotProps={{
|
||||||
|
tooltip: {
|
||||||
|
sx: {
|
||||||
|
background:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)"
|
||||||
|
: "linear-gradient(135deg, #2c3e50 0%, #1a252f 100%)",
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
color: "#ffffff",
|
||||||
|
fontSize: "13px",
|
||||||
|
padding: `${theme.spacing(4)} ${theme.spacing(5)}`,
|
||||||
|
borderRadius: `${theme.shape.borderRadius}px`,
|
||||||
|
boxShadow: "0 8px 24px rgba(0, 0, 0, 0.4)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</MuiTooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface TooltipWithInfoProps extends Omit<StyledTooltipProps, "children"> {
|
||||||
|
iconSize?: number;
|
||||||
|
iconColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TooltipWithInfo = ({
|
||||||
|
iconSize = 14,
|
||||||
|
iconColor,
|
||||||
|
...props
|
||||||
|
}: TooltipWithInfoProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const defaultColor = theme.palette.text.secondary;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip {...props}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
cursor: "help",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Info
|
||||||
|
size={iconSize}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
style={{
|
||||||
|
opacity: 0.7,
|
||||||
|
color: iconColor || defaultColor,
|
||||||
|
transition: "opacity 0.2s ease",
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.opacity = "1";
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.opacity = "0.7";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
export * from "./Breadcrumb";
|
||||||
|
export * from "./BasePage";
|
||||||
|
export * from "./Fallback";
|
||||||
|
export * from "./SkeletonCard";
|
||||||
|
export * from "./BulletPointCheck";
|
||||||
|
export * from "./StatusBox";
|
||||||
|
export * from "./BaseBox";
|
||||||
|
export * from "./Table";
|
||||||
|
export { DataTable as Table } from "./Table";
|
||||||
|
export * from "./StatusLabel";
|
||||||
|
export * from "./PulseDot";
|
||||||
|
export * from "./Dot";
|
||||||
|
export * from "./MonitorStatus";
|
||||||
|
export { default as Icon } from "./Icon";
|
||||||
|
export * from "./Tooltip";
|
||||||
|
export * from "./StatBox";
|
||||||
|
export * from "./BaseChart";
|
||||||
|
export * from "./Gauge";
|
||||||
|
export * from "./Tabs";
|
||||||
|
export * from "./SplitBox";
|
||||||
|
export * from "./TextLink";
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import Autocomplete from "@mui/material/Autocomplete";
|
||||||
|
import type { AutocompleteProps } from "@mui/material/Autocomplete";
|
||||||
|
import { TextField, Checkbox } from "@/Components/v2/inputs";
|
||||||
|
import ListItem from "@mui/material/ListItem";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { FieldLabel } from "./FieldLabel";
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
|
|
||||||
|
type AutoCompleteInputProps = Omit<
|
||||||
|
AutocompleteProps<any, boolean, boolean, boolean>,
|
||||||
|
"renderInput"
|
||||||
|
> & {
|
||||||
|
renderInput?: AutocompleteProps<any, boolean, boolean, boolean>["renderInput"];
|
||||||
|
fieldLabel?: string;
|
||||||
|
required?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AutoCompleteInput = ({
|
||||||
|
fieldLabel,
|
||||||
|
required,
|
||||||
|
renderInput,
|
||||||
|
...props
|
||||||
|
}: AutoCompleteInputProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const multiple = props.multiple;
|
||||||
|
|
||||||
|
const defaultRenderInput = (params: any) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
placeholder="Type to search"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const autocomplete = (
|
||||||
|
<Autocomplete
|
||||||
|
{...props}
|
||||||
|
disableCloseOnSelect={!!multiple}
|
||||||
|
popupIcon={
|
||||||
|
<ChevronDown
|
||||||
|
size={18}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
style={{ marginRight: theme.spacing(3) }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
renderInput={renderInput || defaultRenderInput}
|
||||||
|
getOptionKey={(option) => option.id}
|
||||||
|
renderTags={() => null}
|
||||||
|
renderOption={(props, option, { selected }) => {
|
||||||
|
const { key, ...optionProps } = props;
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
key={key}
|
||||||
|
{...optionProps}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction={"row"}
|
||||||
|
alignItems={"center"}
|
||||||
|
gap={theme.spacing(2)}
|
||||||
|
>
|
||||||
|
{multiple && <Checkbox checked={selected} />}
|
||||||
|
{option.name}
|
||||||
|
</Stack>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
"&.MuiAutocomplete-root .MuiAutocomplete-input": {
|
||||||
|
padding: `0 ${theme.spacing(5)}`,
|
||||||
|
},
|
||||||
|
"& .MuiInputBase-root .MuiAutocomplete-endAdornment": {
|
||||||
|
right: theme.spacing(3),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fieldLabel) {
|
||||||
|
return (
|
||||||
|
<Stack spacing={theme.spacing(2)}>
|
||||||
|
<FieldLabel required={required}>{fieldLabel}</FieldLabel>
|
||||||
|
{autocomplete}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return autocomplete;
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import type { ButtonProps } from "@mui/material/Button";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
export const ButtonInput = ({ sx, ...props }: ButtonProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const outlinedSx =
|
||||||
|
props.variant === "outlined"
|
||||||
|
? {
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
borderColor: theme.palette.divider,
|
||||||
|
"&:hover": {
|
||||||
|
borderColor: theme.palette.text.secondary,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
{...props}
|
||||||
|
sx={{
|
||||||
|
textTransform: "none",
|
||||||
|
height: 34,
|
||||||
|
fontWeight: 400,
|
||||||
|
borderRadius: 2,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
overflow: "hidden",
|
||||||
|
boxShadow: "none",
|
||||||
|
"&:hover": {
|
||||||
|
boxShadow: "none",
|
||||||
|
},
|
||||||
|
...outlinedSx,
|
||||||
|
...sx,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { forwardRef } from "react";
|
||||||
|
import Checkbox from "@mui/material/Checkbox";
|
||||||
|
import type { CheckboxProps } from "@mui/material/Checkbox";
|
||||||
|
import { Square, SquareCheck } from "lucide-react";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
type CheckboxInputProps = CheckboxProps & {
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
export const CheckboxInput = forwardRef<HTMLInputElement, CheckboxInputProps>(
|
||||||
|
function CheckboxInput(
|
||||||
|
{ sx, ...props }: CheckboxInputProps,
|
||||||
|
ref: React.Ref<HTMLInputElement>
|
||||||
|
) {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
{...props}
|
||||||
|
aria-label={props.label}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
ref: ref,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
icon={
|
||||||
|
<Square
|
||||||
|
size={16}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
checkedIcon={
|
||||||
|
<SquareCheck
|
||||||
|
size={16}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
"&.Mui-checked": {
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
"&:hover": { backgroundColor: "transparent" },
|
||||||
|
"& svg": {
|
||||||
|
stroke: "currentColor",
|
||||||
|
},
|
||||||
|
"& svg path, & svg line, & svg polyline, & svg rect": {
|
||||||
|
stroke: "currentColor",
|
||||||
|
fill: "none",
|
||||||
|
},
|
||||||
|
|
||||||
|
...sx,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { MuiColorInput } from "mui-color-input";
|
||||||
|
import type { MuiColorInputProps } from "mui-color-input";
|
||||||
|
import { typographyLevels } from "@/Utils/Theme/v2Palette";
|
||||||
|
import { useTheme } from "@mui/material";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import { FieldLabel } from "@/Components/v2/inputs/FieldLabel";
|
||||||
|
|
||||||
|
interface ColorPickerProps extends MuiColorInputProps {
|
||||||
|
fieldLabel?: string;
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ColorInput = ({ fieldLabel, required, ...props }: ColorPickerProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const input = (
|
||||||
|
<MuiColorInput
|
||||||
|
{...props}
|
||||||
|
sx={{
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
height: 34,
|
||||||
|
fontSize: typographyLevels.base,
|
||||||
|
},
|
||||||
|
"& .MuiOutlinedInput-notchedOutline": {
|
||||||
|
borderColor: theme.palette.divider,
|
||||||
|
},
|
||||||
|
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||||
|
borderColor: theme.palette.divider,
|
||||||
|
},
|
||||||
|
"& .MuiFormHelperText-root": {
|
||||||
|
marginLeft: 0,
|
||||||
|
marginRight: 0,
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
if (fieldLabel) {
|
||||||
|
return (
|
||||||
|
<Stack spacing={theme.spacing(2)}>
|
||||||
|
<FieldLabel required={required}>{fieldLabel}</FieldLabel>
|
||||||
|
{input}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return input;
|
||||||
|
};
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||||
|
import type { DatePickerProps } from "@mui/x-date-pickers/DatePicker";
|
||||||
|
import type { Dayjs } from "dayjs";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { FieldLabel } from "./FieldLabel";
|
||||||
|
import { Calendar } from "lucide-react";
|
||||||
|
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||||
|
import { LocalizationProvider } from "@mui/x-date-pickers";
|
||||||
|
|
||||||
|
interface DatePickerComponentProps extends Omit<DatePickerProps<Dayjs>, "label"> {
|
||||||
|
fieldLabel?: string;
|
||||||
|
required?: boolean;
|
||||||
|
error?: boolean;
|
||||||
|
helperText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DatePickerComponent = ({
|
||||||
|
fieldLabel,
|
||||||
|
required,
|
||||||
|
error,
|
||||||
|
helperText,
|
||||||
|
...props
|
||||||
|
}: DatePickerComponentProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const picker = (
|
||||||
|
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||||
|
<DatePicker
|
||||||
|
{...props}
|
||||||
|
slots={{
|
||||||
|
openPickerIcon: () => (
|
||||||
|
<Calendar
|
||||||
|
size={20}
|
||||||
|
stroke={theme.palette.text.secondary}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
...props.slots,
|
||||||
|
}}
|
||||||
|
slotProps={{
|
||||||
|
switchViewButton: { sx: { display: "none" } },
|
||||||
|
nextIconButton: { sx: { ml: theme.spacing(2) } },
|
||||||
|
day: {
|
||||||
|
sx: {
|
||||||
|
"&.MuiPickersDay-root.Mui-disabled": {
|
||||||
|
color: theme.palette.text.disabled,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
field: {
|
||||||
|
sx: {
|
||||||
|
width: "fit-content",
|
||||||
|
"& > .MuiOutlinedInput-root": {
|
||||||
|
flexDirection: "row-reverse",
|
||||||
|
},
|
||||||
|
"& input": {
|
||||||
|
minHeight: 34,
|
||||||
|
p: 0,
|
||||||
|
pl: theme.spacing(3),
|
||||||
|
pr: theme.spacing(5),
|
||||||
|
},
|
||||||
|
"& fieldset": {
|
||||||
|
borderColor: error ? theme.palette.error.main : theme.palette.divider,
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
},
|
||||||
|
"&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset":
|
||||||
|
{
|
||||||
|
borderColor: error ? theme.palette.error.main : theme.palette.divider,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputAdornment: { sx: { ml: 0 } },
|
||||||
|
openPickerButton: {
|
||||||
|
sx: {
|
||||||
|
py: 0,
|
||||||
|
mr: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...props.slotProps,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</LocalizationProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={theme.spacing(2)}>
|
||||||
|
{fieldLabel && <FieldLabel required={required}>{fieldLabel}</FieldLabel>}
|
||||||
|
{picker}
|
||||||
|
{helperText && (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color={error ? "error" : "text.secondary"}
|
||||||
|
>
|
||||||
|
{helperText}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import Dialog from "@mui/material/Dialog";
|
||||||
|
import type { DialogProps } from "@mui/material/Dialog";
|
||||||
|
import DialogActions from "@mui/material/DialogActions";
|
||||||
|
import DialogContent from "@mui/material/DialogContent";
|
||||||
|
import DialogContentText from "@mui/material/DialogContentText";
|
||||||
|
import DialogTitle from "@mui/material/DialogTitle";
|
||||||
|
import { Button } from "@/Components/v2/inputs";
|
||||||
|
import { typographyLevels } from "@/Utils/Theme/v2Palette";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
export const DialogInput = ({
|
||||||
|
open,
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
confirmColor = "primary",
|
||||||
|
cancelColor = "error",
|
||||||
|
loading = false,
|
||||||
|
cancelText,
|
||||||
|
confirmText,
|
||||||
|
children,
|
||||||
|
maxWidth,
|
||||||
|
fullWidth = false,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
title?: string;
|
||||||
|
content?: string;
|
||||||
|
onConfirm?(item: any): any;
|
||||||
|
onCancel?(item: any): any;
|
||||||
|
confirmColor?: "error" | "primary";
|
||||||
|
cancelColor?: "error" | "primary";
|
||||||
|
loading?: boolean;
|
||||||
|
cancelText?: string;
|
||||||
|
confirmText?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
maxWidth?: DialogProps["maxWidth"];
|
||||||
|
fullWidth?: boolean;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
maxWidth={maxWidth}
|
||||||
|
fullWidth={fullWidth}
|
||||||
|
>
|
||||||
|
{title && <DialogTitle sx={{ fontSize: typographyLevels.l }}>{title}</DialogTitle>}
|
||||||
|
<DialogContent>
|
||||||
|
{content && <DialogContentText>{content}</DialogContentText>}
|
||||||
|
{children}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
loading={loading}
|
||||||
|
variant="contained"
|
||||||
|
color={cancelColor}
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
{cancelText ?? t("common.buttons.cancel")}
|
||||||
|
</Button>
|
||||||
|
{onConfirm && (
|
||||||
|
<Button
|
||||||
|
loading={loading}
|
||||||
|
variant="contained"
|
||||||
|
color={confirmColor}
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
|
{confirmText ?? t("common.buttons.confirm")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
interface FieldLabelProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
required?: boolean;
|
||||||
|
htmlFor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FieldLabel = ({ children, required = false, htmlFor }: FieldLabelProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
if (children === " ") {
|
||||||
|
children = "\u00A0";
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Typography
|
||||||
|
component="label"
|
||||||
|
htmlFor={htmlFor}
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
fontWeight: 500,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
display: "block",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{required && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: theme.palette.error.main,
|
||||||
|
marginLeft: theme.spacing(2),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import { Box, Stack, Typography, IconButton } from "@mui/material";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { Upload, X } from "lucide-react";
|
||||||
|
import { useState, useRef, useCallback } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
interface FileObject {
|
||||||
|
src: string;
|
||||||
|
name: string;
|
||||||
|
file: File;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImageUploadProps {
|
||||||
|
src?: string;
|
||||||
|
onChange?: (file: FileObject | undefined) => void;
|
||||||
|
maxSize?: number;
|
||||||
|
accept?: string[];
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ImageUpload = ({
|
||||||
|
src,
|
||||||
|
onChange,
|
||||||
|
maxSize = 3 * 1024 * 1024,
|
||||||
|
accept = ["jpg", "jpeg", "png"],
|
||||||
|
error,
|
||||||
|
}: ImageUploadProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [localError, setLocalError] = useState<string | null>(null);
|
||||||
|
const [preview, setPreview] = useState<FileObject | null>(null);
|
||||||
|
|
||||||
|
const handleFile = useCallback(
|
||||||
|
(file: File | undefined) => {
|
||||||
|
if (!file) return;
|
||||||
|
const isValidType = accept.some((type) => file.type.includes(type));
|
||||||
|
const isValidSize = file.size <= maxSize;
|
||||||
|
if (!isValidType) {
|
||||||
|
setLocalError(t("common.errors.invalidFileFormat"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isValidSize) {
|
||||||
|
setLocalError(t("common.errors.invalidFileSize"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLocalError(null);
|
||||||
|
const fileObj: FileObject = {
|
||||||
|
src: URL.createObjectURL(file),
|
||||||
|
name: file.name,
|
||||||
|
file,
|
||||||
|
};
|
||||||
|
setPreview(fileObj);
|
||||||
|
onChange?.(fileObj);
|
||||||
|
},
|
||||||
|
[maxSize, accept, onChange, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
setPreview(null);
|
||||||
|
setLocalError(null);
|
||||||
|
onChange?.(undefined);
|
||||||
|
if (inputRef.current) inputRef.current.value = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const displaySrc = src || preview?.src;
|
||||||
|
const displayError = localError || error;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack sx={{ width: "100%", maxWidth: 500 }}>
|
||||||
|
{displaySrc ? (
|
||||||
|
<Stack
|
||||||
|
alignItems="center"
|
||||||
|
gap={1}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src={displaySrc}
|
||||||
|
alt="Preview"
|
||||||
|
sx={{ maxWidth: 250, maxHeight: 250, objectFit: "contain", borderRadius: 1 }}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={handleClear}
|
||||||
|
sx={{ color: theme.palette.error.main }}
|
||||||
|
>
|
||||||
|
<X size={18} />
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<Stack
|
||||||
|
onDragOver={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(true);
|
||||||
|
}}
|
||||||
|
onDragLeave={() => setIsDragging(false)}
|
||||||
|
onDrop={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(false);
|
||||||
|
handleFile(e.dataTransfer.files?.[0]);
|
||||||
|
}}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
gap={1}
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
width: "100%",
|
||||||
|
minHeight: 150,
|
||||||
|
border: "2px dashed",
|
||||||
|
borderRadius: 1,
|
||||||
|
borderColor: isDragging ? theme.palette.primary.main : theme.palette.divider,
|
||||||
|
backgroundColor: isDragging ? theme.palette.action.hover : "transparent",
|
||||||
|
transition: "0.2s",
|
||||||
|
cursor: "pointer",
|
||||||
|
"&:hover": {
|
||||||
|
borderColor: theme.palette.primary.main,
|
||||||
|
backgroundColor: theme.palette.action.hover,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="file"
|
||||||
|
accept={accept.map((ext) => `.${ext}`).join(",")}
|
||||||
|
onChange={(e) => handleFile(e.target.files?.[0])}
|
||||||
|
style={{ position: "absolute", inset: 0, opacity: 0, cursor: "pointer" }}
|
||||||
|
/>
|
||||||
|
<Upload size={24} />
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
component="span"
|
||||||
|
variant="body2"
|
||||||
|
color="primary"
|
||||||
|
fontWeight={500}
|
||||||
|
>
|
||||||
|
{t("common.imageUpload.clickToUpload")}
|
||||||
|
</Typography>{" "}
|
||||||
|
{t("common.imageUpload.orDragAndDrop")}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.disabled"
|
||||||
|
>
|
||||||
|
{accept.join(", ").toUpperCase()} • {t("common.imageUpload.maxSize")}{" "}
|
||||||
|
{Math.round(maxSize / 1024 / 1024)}MB
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
{displayError && (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="error"
|
||||||
|
sx={{ mt: 1 }}
|
||||||
|
>
|
||||||
|
{displayError}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import { Select } from "@/Components/v2/inputs";
|
||||||
|
import "flag-icons/css/flag-icons.min.css";
|
||||||
|
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useTheme } from "@mui/material";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { setLanguage } from "@/Features/UI/uiSlice";
|
||||||
|
import type { RootState } from "@/Types/state";
|
||||||
|
|
||||||
|
const langMap: Record<string, string> = {
|
||||||
|
cs: "cz",
|
||||||
|
ja: "jp",
|
||||||
|
uk: "ua",
|
||||||
|
vi: "vn",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LanguageSelector = () => {
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
const theme = useTheme();
|
||||||
|
const { language = "en" } = useSelector((state: RootState) => state.ui);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const handleChange = (event: any) => {
|
||||||
|
const newLang = event.target.value;
|
||||||
|
dispatch(setLanguage(newLang));
|
||||||
|
};
|
||||||
|
|
||||||
|
const languages = Object.keys(i18n.options.resources || {});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
value={language}
|
||||||
|
onChange={handleChange}
|
||||||
|
size="small"
|
||||||
|
// sx={{
|
||||||
|
// minWidth: 80,
|
||||||
|
// "& .MuiSelect-select": {
|
||||||
|
// display: "flex",
|
||||||
|
// alignItems: "center",
|
||||||
|
// justifyContent: "center",
|
||||||
|
// },
|
||||||
|
// "& .MuiSelect-icon": {
|
||||||
|
// alignSelf: "center",
|
||||||
|
// },
|
||||||
|
// }}
|
||||||
|
>
|
||||||
|
{languages.map((lang) => {
|
||||||
|
let parsedLang = lang === "en" ? "gb" : lang;
|
||||||
|
|
||||||
|
if (parsedLang.includes("-")) {
|
||||||
|
parsedLang = parsedLang.split("-")[1].toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedLang = langMap[parsedLang] || parsedLang;
|
||||||
|
|
||||||
|
const flag = parsedLang ? `fi fi-${parsedLang}` : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
key={lang}
|
||||||
|
value={lang}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={theme.spacing(2)}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
component="span"
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{flag && <span className={flag} />}
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
component="span"
|
||||||
|
sx={{ textTransform: "uppercase" }}
|
||||||
|
>
|
||||||
|
{lang}
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LanguageSelector;
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import Radio from "@mui/material/Radio";
|
||||||
|
import type { RadioProps } from "@mui/material/Radio";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { Circle, CircleDot } from "lucide-react";
|
||||||
|
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
|
interface RadioInputProps extends RadioProps {}
|
||||||
|
|
||||||
|
export const RadioInput = ({ ...props }: RadioInputProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Radio
|
||||||
|
{...props}
|
||||||
|
icon={
|
||||||
|
<Circle
|
||||||
|
size={16}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
checkedIcon={
|
||||||
|
<CircleDot
|
||||||
|
size={14}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
padding: 0,
|
||||||
|
mt: theme.spacing(0.5),
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
"&.Mui-checked": {
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
"& svg circle": {
|
||||||
|
fill: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"& .MuiSvgIcon-root": {
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
"& svg": {
|
||||||
|
stroke: "currentColor",
|
||||||
|
},
|
||||||
|
"& svg path, & svg line, & svg polyline, & svg rect, & svg circle": {
|
||||||
|
stroke: "currentColor",
|
||||||
|
fill: "none",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RadioWithDescription = ({
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
...props
|
||||||
|
}: RadioInputProps & { label: string; description: string }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<FormControlLabel
|
||||||
|
control={<RadioInput {...props} />}
|
||||||
|
label={
|
||||||
|
<>
|
||||||
|
<Typography component="p">{label}</Typography>
|
||||||
|
<Typography
|
||||||
|
component="h6"
|
||||||
|
color={theme.palette.text.secondary}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
alignItems: "flex-start",
|
||||||
|
p: theme.spacing(2.5),
|
||||||
|
m: theme.spacing(-2.5),
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
"& .MuiButtonBase-root": {
|
||||||
|
p: 0,
|
||||||
|
mr: theme.spacing(6),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import Select from "@mui/material/Select";
|
||||||
|
import type { SelectProps } from "@mui/material/Select";
|
||||||
|
import React, { forwardRef } from "react";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import { FieldLabel } from "./FieldLabel";
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
|
interface SelectInputProps<T> extends Omit<SelectProps<T>, "label"> {
|
||||||
|
fieldLabel?: string;
|
||||||
|
required?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
placeholderColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SelectInputInner = <T,>(
|
||||||
|
{ fieldLabel, required, placeholder, placeholderColor, ...props }: SelectInputProps<T>,
|
||||||
|
ref: React.ForwardedRef<HTMLDivElement>
|
||||||
|
) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const emptyPlaceholderColor = placeholderColor || theme.palette.text.disabled;
|
||||||
|
|
||||||
|
const renderValue = (selected: unknown) => {
|
||||||
|
const isMultiple = Boolean((props as { multiple?: boolean }).multiple);
|
||||||
|
const isEmpty = isMultiple
|
||||||
|
? !Array.isArray(selected) || selected.length === 0
|
||||||
|
: selected === undefined || selected === null || selected === "";
|
||||||
|
|
||||||
|
if (isEmpty && placeholder) {
|
||||||
|
return <Typography sx={{ color: emptyPlaceholderColor }}>{placeholder}</Typography>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMultiple) {
|
||||||
|
const items: string[] = Array.isArray(selected) ? selected : [];
|
||||||
|
const capitalized = items.map(
|
||||||
|
(item) => item.charAt(0).toUpperCase() + item.slice(1)
|
||||||
|
);
|
||||||
|
return <Typography>{capitalized.join(" | ")}</Typography>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodes = React.Children.toArray(props.children as React.ReactNode);
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (!React.isValidElement(node)) continue;
|
||||||
|
const el = node as React.ReactElement<{
|
||||||
|
value?: unknown;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}>;
|
||||||
|
if (el.props?.value === selected) {
|
||||||
|
return (el.props.children ?? selected) as React.ReactNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selected as React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const select = (
|
||||||
|
<Select<T>
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
displayEmpty
|
||||||
|
renderValue={renderValue}
|
||||||
|
inputProps={{
|
||||||
|
...(props.inputProps || {}),
|
||||||
|
"aria-placeholder": placeholder,
|
||||||
|
}}
|
||||||
|
IconComponent={() => (
|
||||||
|
<ChevronDown
|
||||||
|
size={18}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
style={{ marginRight: theme.spacing(3) }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
sx={{
|
||||||
|
height: "34px",
|
||||||
|
"& .MuiSelect-select": {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "100%",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
},
|
||||||
|
"& .MuiSelect-icon": {
|
||||||
|
right: theme.spacing(3),
|
||||||
|
},
|
||||||
|
"& .MuiOutlinedInput-notchedOutline": {
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
borderColor: theme.palette.divider,
|
||||||
|
},
|
||||||
|
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||||
|
borderColor: theme.palette.divider,
|
||||||
|
},
|
||||||
|
...props.sx,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fieldLabel) {
|
||||||
|
return (
|
||||||
|
<Stack spacing={theme.spacing(2)}>
|
||||||
|
<FieldLabel required={required}>{fieldLabel}</FieldLabel>
|
||||||
|
{select}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return select;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SelectInput = forwardRef(SelectInputInner) as <T>(
|
||||||
|
props: SelectInputProps<T> & { ref?: React.ForwardedRef<HTMLDivElement> }
|
||||||
|
) => React.ReactElement;
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { forwardRef } from "react";
|
||||||
|
import Slider from "@mui/material/Slider";
|
||||||
|
import type { SliderProps } from "@mui/material/Slider";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import { FieldLabel } from "./FieldLabel";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import type { ResponsiveStyleValue } from "@mui/system";
|
||||||
|
|
||||||
|
interface SliderInputProps extends SliderProps {
|
||||||
|
sx?: SliderProps["sx"];
|
||||||
|
showValue?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SliderInput = forwardRef<HTMLSpanElement, SliderInputProps>(
|
||||||
|
({ sx, showValue = false, ...props }, ref) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const additionalSx = Array.isArray(sx) ? sx : sx ? [sx] : [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
gap={theme.spacing(8)}
|
||||||
|
direction={"row"}
|
||||||
|
alignItems={"center"}
|
||||||
|
>
|
||||||
|
{showValue && <Typography>{props.value}</Typography>}
|
||||||
|
<Slider
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
sx={[
|
||||||
|
{
|
||||||
|
"& .MuiSlider-track": {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
border: "none",
|
||||||
|
},
|
||||||
|
"& .MuiSlider-rail": {
|
||||||
|
backgroundColor: theme.palette.grey[300],
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
"& .MuiSlider-thumb": {
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
"&:hover, &.Mui-focusVisible": {
|
||||||
|
boxShadow: `0 0 0 8px ${theme.palette.primary.main}20`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"& .MuiSlider-valueLabel": {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...additionalSx,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
interface SliderWithLabelProps extends SliderProps {
|
||||||
|
fieldLabel?: string;
|
||||||
|
required?: boolean;
|
||||||
|
showValue?: boolean;
|
||||||
|
sliderMaxWidth?: ResponsiveStyleValue<number | string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SliderWithLabel = forwardRef<HTMLSpanElement, SliderWithLabelProps>(
|
||||||
|
(
|
||||||
|
{ fieldLabel, required, showValue = true, value, sliderMaxWidth = "100%", ...props },
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const labelText = fieldLabel;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={theme.spacing(2)}>
|
||||||
|
{fieldLabel && <FieldLabel required={required}>{labelText}</FieldLabel>}
|
||||||
|
<Box maxWidth={sliderMaxWidth}>
|
||||||
|
<SliderInput
|
||||||
|
{...props}
|
||||||
|
showValue={showValue}
|
||||||
|
value={value}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { forwardRef } from "react";
|
||||||
|
import Switch from "@mui/material/Switch";
|
||||||
|
import type { SwitchProps } from "@mui/material/Switch";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
interface SwitchComponentProps extends SwitchProps {
|
||||||
|
dualOption?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SwitchComponent = forwardRef<HTMLInputElement, SwitchComponentProps>(
|
||||||
|
function SwitchComponent({ sx, dualOption = false, ...props }, ref) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const additionalSx = Array.isArray(sx) ? sx : sx ? [sx] : [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
{...props}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
ref: ref,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
sx={[
|
||||||
|
{
|
||||||
|
"& .MuiSwitch-switchBase": {
|
||||||
|
"&.Mui-checked": {
|
||||||
|
color: "#E0E0E0",
|
||||||
|
"& + .MuiSwitch-track": {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
opacity: 1,
|
||||||
|
border: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...(dualOption && {
|
||||||
|
"& .MuiSwitch-track": {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
...additionalSx,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
|
||||||
|
import { Moon, Sun } from "lucide-react";
|
||||||
|
|
||||||
|
import { setMode } from "@/Features/UI/uiSlice.js";
|
||||||
|
import { useTheme } from "@mui/material";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import type { RootState } from "@/Types/state";
|
||||||
|
|
||||||
|
export const SwitchTheme = () => {
|
||||||
|
const mode = useSelector((state: RootState) => state.ui.mode);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const handleChange = () => {
|
||||||
|
dispatch(setMode(mode === "light" ? "dark" : "light"));
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
id="theme-toggle"
|
||||||
|
onClick={handleChange}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
"& svg": {
|
||||||
|
transition: "stroke 0.2s ease",
|
||||||
|
},
|
||||||
|
"&:hover svg path, &:hover svg line, &:hover svg polyline, &:hover svg rect, &:hover svg circle":
|
||||||
|
{
|
||||||
|
stroke: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{mode === "light" ? (
|
||||||
|
<Moon
|
||||||
|
size={16}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Sun
|
||||||
|
size={16}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { forwardRef } from "react";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import type { TextFieldProps } from "@mui/material";
|
||||||
|
import { typographyLevels } from "@/Utils/Theme/v2Palette";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import { FieldLabel } from "./FieldLabel";
|
||||||
|
|
||||||
|
interface TextInputProps extends Omit<TextFieldProps, "label"> {
|
||||||
|
fieldLabel?: string;
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(function TextInput(
|
||||||
|
{ fieldLabel, required, ...props },
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const input = (
|
||||||
|
<TextField
|
||||||
|
{...props}
|
||||||
|
inputRef={ref}
|
||||||
|
sx={{
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
height: 34,
|
||||||
|
fontSize: typographyLevels.base,
|
||||||
|
},
|
||||||
|
"& .MuiOutlinedInput-notchedOutline": {
|
||||||
|
borderColor: theme.palette.divider,
|
||||||
|
},
|
||||||
|
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||||
|
borderColor: theme.palette.divider,
|
||||||
|
},
|
||||||
|
"& .MuiFormHelperText-root": {
|
||||||
|
marginLeft: 0,
|
||||||
|
marginRight: 0,
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fieldLabel) {
|
||||||
|
return (
|
||||||
|
<Stack spacing={theme.spacing(2)}>
|
||||||
|
<FieldLabel required={required}>{fieldLabel}</FieldLabel>
|
||||||
|
{input}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return input;
|
||||||
|
});
|
||||||
|
TextInput.displayName = "TextInput";
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker";
|
||||||
|
import type { MobileTimePickerProps } from "@mui/x-date-pickers/MobileTimePicker";
|
||||||
|
import type { Dayjs } from "dayjs";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import { FieldLabel } from "./FieldLabel";
|
||||||
|
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||||
|
import { LocalizationProvider } from "@mui/x-date-pickers";
|
||||||
|
|
||||||
|
interface TimePickerComponentProps extends Omit<MobileTimePickerProps<Dayjs>, "label"> {
|
||||||
|
fieldLabel?: string;
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TimePickerComponent = ({
|
||||||
|
fieldLabel,
|
||||||
|
required,
|
||||||
|
...props
|
||||||
|
}: TimePickerComponentProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const picker = (
|
||||||
|
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||||
|
<MobileTimePicker
|
||||||
|
{...props}
|
||||||
|
slotProps={{
|
||||||
|
field: {
|
||||||
|
sx: {
|
||||||
|
width: "fit-content",
|
||||||
|
"& input": {
|
||||||
|
minHeight: 34,
|
||||||
|
p: 0,
|
||||||
|
px: theme.spacing(5),
|
||||||
|
},
|
||||||
|
"& fieldset": {
|
||||||
|
borderColor: theme.palette.divider,
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
},
|
||||||
|
"&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset":
|
||||||
|
{
|
||||||
|
borderColor: theme.palette.divider,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...props.slotProps,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</LocalizationProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fieldLabel) {
|
||||||
|
return (
|
||||||
|
<Stack spacing={theme.spacing(2)}>
|
||||||
|
<FieldLabel required={required}>{fieldLabel}</FieldLabel>
|
||||||
|
{picker}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return picker;
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import ToggleButton from "@mui/material/ToggleButton";
|
||||||
|
import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
|
||||||
|
import type { ToggleButtonProps } from "@mui/material/ToggleButton";
|
||||||
|
import type { ToggleButtonGroupProps } from "@mui/material/ToggleButtonGroup";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
export const ToggleButtonInput = ({ sx, ...props }: ToggleButtonProps) => {
|
||||||
|
return (
|
||||||
|
<ToggleButton
|
||||||
|
{...props}
|
||||||
|
sx={{
|
||||||
|
px: 8,
|
||||||
|
textTransform: "none",
|
||||||
|
height: 34,
|
||||||
|
fontWeight: 400,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
overflow: "hidden",
|
||||||
|
boxShadow: "none",
|
||||||
|
"&:hover": {
|
||||||
|
boxShadow: "none",
|
||||||
|
},
|
||||||
|
...sx,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ToggleButtonGroupInput = ({ sx, ...props }: ToggleButtonGroupProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<ToggleButtonGroup
|
||||||
|
{...props}
|
||||||
|
sx={{
|
||||||
|
"& .MuiToggleButtonGroup-grouped": {
|
||||||
|
borderColor: theme.palette.divider,
|
||||||
|
|
||||||
|
borderRadius: 0,
|
||||||
|
"&:first-of-type": {
|
||||||
|
borderTopLeftRadius: 2,
|
||||||
|
borderBottomLeftRadius: 2,
|
||||||
|
},
|
||||||
|
"&:last-of-type": {
|
||||||
|
borderTopRightRadius: 2,
|
||||||
|
borderBottomRightRadius: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...sx,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export { ButtonInput as Button } from "./Button";
|
||||||
|
export * from "./FieldLabel";
|
||||||
|
export { SelectInput as Select } from "./Select";
|
||||||
|
export { CheckboxInput as Checkbox } from "./Checkbox";
|
||||||
|
export { AutoCompleteInput as Autocomplete } from "./AutoComplete";
|
||||||
|
export { TextInput as TextField } from "./TextInput";
|
||||||
|
export {
|
||||||
|
ToggleButtonInput as ToggleButton,
|
||||||
|
ToggleButtonGroupInput as ToggleButtonGroup,
|
||||||
|
} from "./ToggleButton";
|
||||||
|
export { DialogInput as Dialog } from "./Dialog";
|
||||||
|
export * from "./Radio";
|
||||||
|
export * from "./Switch";
|
||||||
|
export * from "./Slider";
|
||||||
|
export * from "./ImageUpload";
|
||||||
|
export * from "./ColorPicker";
|
||||||
|
export { DatePickerComponent as DatePicker } from "./DatePicker";
|
||||||
|
export { TimePickerComponent as TimePicker } from "./TimePicker";
|
||||||
|
export * from "./LanguageSelector";
|
||||||
|
export * from "./SwitchTheme";
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import { Select, Button } from "@/Components/v2/inputs";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
|
|
||||||
|
import type { MonitorType } from "@/Types/Monitor";
|
||||||
|
import { Typography, useTheme } from "@mui/material";
|
||||||
|
|
||||||
|
const types = ["http", "ping", "port", "docker", "game"];
|
||||||
|
const statuses = ["up", "down"];
|
||||||
|
const states = ["active", "paused"];
|
||||||
|
|
||||||
|
export const ControlsFilter = ({
|
||||||
|
showTypes = true,
|
||||||
|
selectedTypes,
|
||||||
|
setSelectedTypes,
|
||||||
|
selectedStatus,
|
||||||
|
setSelectedStatus,
|
||||||
|
selectedState,
|
||||||
|
setSelectedState,
|
||||||
|
onClearFilters,
|
||||||
|
}: {
|
||||||
|
showTypes?: boolean;
|
||||||
|
selectedTypes?: MonitorType[];
|
||||||
|
setSelectedTypes?: React.Dispatch<React.SetStateAction<MonitorType[]>>;
|
||||||
|
selectedStatus: string;
|
||||||
|
setSelectedStatus: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
selectedState: string;
|
||||||
|
setSelectedState: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
onClearFilters: () => void;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||||
|
const isFilterActive =
|
||||||
|
(selectedTypes?.length ?? 0) > 0 || selectedStatus !== "" || selectedState !== "";
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
direction={isSmall ? "column" : "row"}
|
||||||
|
gap={theme.spacing(2)}
|
||||||
|
>
|
||||||
|
{showTypes && setSelectedTypes && (
|
||||||
|
<Select
|
||||||
|
multiple
|
||||||
|
placeholder="Type"
|
||||||
|
value={selectedTypes ?? []}
|
||||||
|
onChange={(e) => setSelectedTypes(e.target.value as MonitorType[])}
|
||||||
|
>
|
||||||
|
{types.map((type) => (
|
||||||
|
<MenuItem
|
||||||
|
key={type}
|
||||||
|
value={type}
|
||||||
|
>
|
||||||
|
<Typography textTransform={"capitalize"}>{type}</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
<Select
|
||||||
|
placeholder="Status"
|
||||||
|
value={selectedStatus}
|
||||||
|
onChange={(e) => setSelectedStatus(e.target.value)}
|
||||||
|
>
|
||||||
|
{statuses.map((status) => (
|
||||||
|
<MenuItem
|
||||||
|
key={status}
|
||||||
|
value={status}
|
||||||
|
>
|
||||||
|
<Typography textTransform={"capitalize"}>{status}</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
placeholder="State"
|
||||||
|
value={selectedState}
|
||||||
|
onChange={(e) => setSelectedState(e.target.value)}
|
||||||
|
>
|
||||||
|
{states.map((state) => (
|
||||||
|
<MenuItem
|
||||||
|
key={state}
|
||||||
|
value={state}
|
||||||
|
>
|
||||||
|
<Typography textTransform={"capitalize"}>{state}</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
{isFilterActive && (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={onClearFilters}
|
||||||
|
>
|
||||||
|
Clear Filters
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import { Icon, MonitorStatus } from "@/Components/v2/design-elements";
|
||||||
|
import { Button } from "@/Components/v2/inputs";
|
||||||
|
import { Settings, Pause, Play, Mail, Bug, Trash } from "lucide-react";
|
||||||
|
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { usePost } from "@/Hooks/UseApi";
|
||||||
|
|
||||||
|
import type { Monitor } from "@/Types/Monitor.js";
|
||||||
|
interface BaseHeaderProps {
|
||||||
|
monitor: Monitor;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BaseHeader = ({ monitor, children }: React.PropsWithChildren<BaseHeaderProps>) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
spacing={{ xs: theme.spacing(8), md: 0 }}
|
||||||
|
direction={{ xs: "column", md: "row" }}
|
||||||
|
alignItems={"center"}
|
||||||
|
justifyContent={"space-between"}
|
||||||
|
>
|
||||||
|
<MonitorStatus monitor={monitor} />
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface HeaderMonitorControlsProps {
|
||||||
|
path: string;
|
||||||
|
monitor?: Monitor | null;
|
||||||
|
isAdmin: boolean;
|
||||||
|
refetch: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HeaderMonitorControls = ({
|
||||||
|
path,
|
||||||
|
monitor,
|
||||||
|
isAdmin,
|
||||||
|
refetch,
|
||||||
|
}: HeaderMonitorControlsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const theme = useTheme();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const {
|
||||||
|
post,
|
||||||
|
loading: isPosting,
|
||||||
|
// error: postError,
|
||||||
|
} = usePost<any, Monitor>();
|
||||||
|
|
||||||
|
if (!monitor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<BaseHeader monitor={monitor}>
|
||||||
|
<Stack
|
||||||
|
width={{ xs: "100%", md: "auto" }}
|
||||||
|
direction={{ xs: "column", md: "row" }}
|
||||||
|
gap={theme.spacing(2)}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
loading={isPosting}
|
||||||
|
startIcon={<Icon icon={Mail} />}
|
||||||
|
onClick={async () => {
|
||||||
|
await post(`/notifications/test/all`, { monitorId: monitor.id });
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("common.buttons.testNotifications")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
startIcon={<Icon icon={Bug} />}
|
||||||
|
onClick={() => {
|
||||||
|
navigate(`/incidents/${monitor?.id}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("common.buttons.incidents")}
|
||||||
|
</Button>
|
||||||
|
{isAdmin && (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
loading={isPosting}
|
||||||
|
startIcon={monitor?.isActive ? <Icon icon={Pause} /> : <Icon icon={Play} />}
|
||||||
|
onClick={async () => {
|
||||||
|
await post(`/monitors/pause/${monitor.id}`, {});
|
||||||
|
await refetch();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{monitor?.isActive ? t("common.buttons.pause") : t("common.buttons.resume")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{isAdmin && (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
startIcon={<Icon icon={Settings} />}
|
||||||
|
onClick={() => navigate(`/${path}/configure/${monitor.id}`)}
|
||||||
|
>
|
||||||
|
{t("common.buttons.configure")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</BaseHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface HeaderDeleteControlsProps {
|
||||||
|
monitor?: Monitor | null;
|
||||||
|
isAdmin: boolean;
|
||||||
|
refetch: Function;
|
||||||
|
onDelete?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HeaderDeleteControls = ({
|
||||||
|
monitor,
|
||||||
|
isAdmin,
|
||||||
|
refetch,
|
||||||
|
onDelete,
|
||||||
|
}: HeaderDeleteControlsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const theme = useTheme();
|
||||||
|
const {
|
||||||
|
post,
|
||||||
|
loading: isPosting,
|
||||||
|
// error: postError,
|
||||||
|
} = usePost<any, Monitor>();
|
||||||
|
|
||||||
|
if (!monitor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<BaseHeader monitor={monitor}>
|
||||||
|
<Stack
|
||||||
|
width={{ xs: "100%", md: "auto" }}
|
||||||
|
direction={{ xs: "column", md: "row" }}
|
||||||
|
gap={theme.spacing(2)}
|
||||||
|
>
|
||||||
|
{isAdmin && (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
loading={isPosting}
|
||||||
|
startIcon={monitor?.isActive ? <Icon icon={Pause} /> : <Icon icon={Play} />}
|
||||||
|
onClick={async () => {
|
||||||
|
await post(`/monitors/pause/${monitor.id}`, {});
|
||||||
|
await refetch();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{monitor?.isActive ? t("common.buttons.pause") : t("common.buttons.resume")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{isAdmin && (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
startIcon={<Icon icon={Trash} />}
|
||||||
|
onClick={() => {
|
||||||
|
onDelete?.();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("common.buttons.delete")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</BaseHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
Loaded 100 of 630 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user