(analytics): Frontend updates for Application dashboard and data source CRUD v2 (#2890)

* Updating frontend for analytics/monitoring app. dashboard CRUD-v2
* removing dev setup changes, updated dashboard links and added portal-dashboards.
* Fixing deep-scan issues.
* Minor fix
* fix package-lock
* fixing package-lock using lts of node & npm via nvm

Signed-off-by: ishangupta-ds <ishan@chaosnative.com>
This commit is contained in:
Ishan Gupta 2021-06-14 14:23:58 +05:30 committed by GitHub
parent ac79879512
commit 3ef78a10e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
122 changed files with 8896 additions and 3286 deletions

View File

@ -5461,6 +5461,15 @@
"resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.5.tgz",
"integrity": "sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA=="
},
"bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"optional": true,
"requires": {
"file-uri-to-path": "1.0.0"
}
},
"bit-twiddle": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bit-twiddle/-/bit-twiddle-1.0.2.tgz",
@ -9665,6 +9674,12 @@
"schema-utils": "^2.5.0"
}
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"optional": true
},
"filesize": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.0.1.tgz",
@ -12816,7 +12831,11 @@
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"optional": true
"optional": true,
"requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1"
}
}
}
},
@ -15731,6 +15750,12 @@
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
},
"nan": {
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
"optional": true
},
"nanomatch": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@ -22290,7 +22315,11 @@
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"optional": true
"optional": true,
"requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1"
}
},
"glob-parent": {
"version": "3.1.0",
@ -22608,7 +22637,11 @@
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"optional": true
"optional": true,
"requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1"
}
},
"glob-parent": {
"version": "3.1.0",

View File

@ -0,0 +1,3 @@
<svg width="10" height="8" viewBox="0 0 10 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.82918 7.73636C3.73372 7.83237 3.60349 7.88594 3.46821 7.88594C3.33292 7.88594 3.20269 7.83237 3.10723 7.73636L0.224383 4.85304C-0.0747942 4.55386 -0.0747942 4.06873 0.224383 3.77011L0.585358 3.40905C0.884628 3.10987 1.3692 3.10987 1.66838 3.40905L3.46821 5.20897L8.3316 0.345476C8.63087 0.0462996 9.11591 0.0462996 9.41462 0.345476L9.7756 0.706545C10.0748 1.00572 10.0748 1.49076 9.7756 1.78947L3.82918 7.73636Z" fill="#109B67"/>
</svg>

After

Width:  |  Height:  |  Size: 543 B

View File

@ -1,4 +1,4 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.67436 10.295C9.80403 10.295 10.7198 9.37923 10.7198 8.24956C10.7198 7.11988 9.80403 6.2041 8.67436 6.2041C7.54469 6.2041 6.62891 7.11988 6.62891 8.24956C6.62891 9.37923 7.54469 10.295 8.67436 10.295Z" stroke="#777777" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.7203 10.2955C13.6295 10.5011 13.6024 10.7292 13.6425 10.9504C13.6826 11.1716 13.7881 11.3757 13.9453 11.5364L13.9862 11.5773C14.113 11.7039 14.2135 11.8543 14.2822 12.0199C14.3508 12.1854 14.3861 12.3628 14.3861 12.542C14.3861 12.7212 14.3508 12.8987 14.2822 13.0642C14.2135 13.2298 14.113 13.3802 13.9862 13.5068C13.8595 13.6336 13.7091 13.7342 13.5436 13.8028C13.378 13.8714 13.2006 13.9068 13.0214 13.9068C12.8422 13.9068 12.6647 13.8714 12.4992 13.8028C12.3337 13.7342 12.1833 13.6336 12.0566 13.5068L12.0157 13.4659C11.855 13.3087 11.6509 13.2033 11.4298 13.1632C11.2086 13.1231 10.9805 13.1501 10.7748 13.2409C10.5731 13.3273 10.4012 13.4708 10.28 13.6538C10.1589 13.8367 10.0939 14.0511 10.093 14.2705V14.3864C10.093 14.748 9.94932 15.0949 9.69359 15.3506C9.43785 15.6063 9.09101 15.75 8.72935 15.75C8.36769 15.75 8.02085 15.6063 7.76511 15.3506C7.50938 15.0949 7.36571 14.748 7.36571 14.3864V14.325C7.36043 14.0993 7.28739 13.8805 7.15606 13.6968C7.02474 13.5132 6.84122 13.3734 6.62935 13.2955C6.4237 13.2047 6.19558 13.1776 5.9744 13.2177C5.75322 13.2578 5.54913 13.3633 5.38844 13.5205L5.34753 13.5614C5.22089 13.6882 5.07049 13.7887 4.90495 13.8574C4.73941 13.926 4.56196 13.9613 4.38276 13.9613C4.20356 13.9613 4.02611 13.926 3.86057 13.8574C3.69503 13.7887 3.54463 13.6882 3.41799 13.5614C3.2912 13.4347 3.19062 13.2843 3.122 13.1188C3.05337 12.9532 3.01805 12.7758 3.01805 12.5966C3.01805 12.4174 3.05337 12.2399 3.122 12.0744C3.19062 11.9089 3.2912 11.7585 3.41799 11.6318L3.4589 11.5909C3.61608 11.4302 3.72152 11.2261 3.76163 11.0049C3.80173 10.7838 3.77466 10.5556 3.6839 10.35C3.59747 10.1483 3.45396 9.97635 3.27103 9.85521C3.08811 9.73407 2.87375 9.66906 2.65435 9.66818H2.53844C2.17678 9.66818 1.82994 9.52451 1.5742 9.26878C1.31847 9.01305 1.1748 8.6662 1.1748 8.30455C1.1748 7.94289 1.31847 7.59604 1.5742 7.34031C1.82994 7.08458 2.17678 6.94091 2.53844 6.94091H2.5998C2.82548 6.93563 3.04435 6.86258 3.22796 6.73126C3.41157 6.59993 3.55143 6.41641 3.62935 6.20455C3.72011 5.9989 3.74718 5.77078 3.70708 5.5496C3.66698 5.32842 3.56153 5.12433 3.40435 4.96364L3.36344 4.92273C3.23666 4.79608 3.13607 4.64569 3.06745 4.48015C2.99883 4.3146 2.9635 4.13716 2.9635 3.95795C2.9635 3.77875 2.99883 3.60131 3.06745 3.43576C3.13607 3.27022 3.23666 3.11983 3.36344 2.99318C3.49009 2.8664 3.64048 2.76582 3.80602 2.69719C3.97157 2.62857 4.14901 2.59325 4.32821 2.59325C4.50742 2.59325 4.68486 2.62857 4.85041 2.69719C5.01595 2.76582 5.16634 2.8664 5.29299 2.99318L5.3339 3.03409C5.49459 3.19128 5.69868 3.29672 5.91986 3.33682C6.14104 3.37693 6.36916 3.34985 6.5748 3.25909H6.62935C6.83101 3.17266 7.003 3.02915 7.12414 2.84623C7.24528 2.6633 7.31029 2.44895 7.31117 2.22955V2.11364C7.31117 1.75198 7.45484 1.40513 7.71057 1.1494C7.9663 0.893668 8.31315 0.75 8.6748 0.75C9.03646 0.75 9.38331 0.893668 9.63904 1.1494C9.89477 1.40513 10.0384 1.75198 10.0384 2.11364V2.175C10.0393 2.3944 10.1043 2.60876 10.2255 2.79168C10.3466 2.97461 10.5186 3.11812 10.7203 3.20455C10.9259 3.29531 11.154 3.32238 11.3752 3.28228C11.5964 3.24217 11.8005 3.13673 11.9612 2.97955L12.0021 2.93864C12.1287 2.81185 12.2791 2.71127 12.4447 2.64265C12.6102 2.57402 12.7876 2.5387 12.9669 2.5387C13.1461 2.5387 13.3235 2.57402 13.489 2.64265C13.6546 2.71127 13.805 2.81185 13.9316 2.93864C14.0584 3.06528 14.159 3.21567 14.2276 3.38122C14.2962 3.54676 14.3316 3.72421 14.3316 3.90341C14.3316 4.08261 14.2962 4.26006 14.2276 4.4256C14.159 4.59114 14.0584 4.74154 13.9316 4.86818L13.8907 4.90909C13.7335 5.06978 13.6281 5.27387 13.588 5.49505C13.5479 5.71623 13.575 5.94435 13.6657 6.15V6.20455C13.7521 6.40621 13.8957 6.57819 14.0786 6.69934C14.2615 6.82048 14.4759 6.88549 14.6953 6.88636H14.8112C15.1728 6.88636 15.5197 7.03003 15.7754 7.28576C16.0311 7.5415 16.1748 7.88834 16.1748 8.25C16.1748 8.61166 16.0311 8.9585 15.7754 9.21424C15.5197 9.46997 15.1728 9.61364 14.8112 9.61364H14.7498C14.5304 9.61451 14.316 9.67952 14.1331 9.80066C13.9502 9.92181 13.8067 10.0938 13.7203 10.2955V10.2955Z" stroke="#777777" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.50004 10.545C9.62972 10.545 10.5455 9.62923 10.5455 8.49956C10.5455 7.36988 9.62972 6.4541 8.50004 6.4541C7.37037 6.4541 6.45459 7.36988 6.45459 8.49956C6.45459 9.62923 7.37037 10.545 8.50004 10.545Z" stroke="#1C0732" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.5455 10.5455C13.4547 10.7511 13.4276 10.9792 13.4677 11.2004C13.5078 11.4216 13.6133 11.6257 13.7705 11.7864L13.8114 11.8273C13.9382 11.9539 14.0387 12.1043 14.1074 12.2699C14.176 12.4354 14.2113 12.6128 14.2113 12.792C14.2113 12.9712 14.176 13.1487 14.1074 13.3142C14.0387 13.4798 13.9382 13.6302 13.8114 13.7568C13.6847 13.8836 13.5343 13.9842 13.3688 14.0528C13.2032 14.1214 13.0258 14.1568 12.8466 14.1568C12.6674 14.1568 12.4899 14.1214 12.3244 14.0528C12.1589 13.9842 12.0085 13.8836 11.8818 13.7568L11.8409 13.7159C11.6802 13.5587 11.4761 13.4533 11.2549 13.4132C11.0338 13.3731 10.8056 13.4001 10.6 13.4909C10.3983 13.5773 10.2264 13.7208 10.1052 13.9038C9.98407 14.0867 9.91906 14.3011 9.91818 14.5205V14.6364C9.91818 14.998 9.77451 15.3449 9.51878 15.6006C9.26305 15.8563 8.9162 16 8.55455 16C8.19289 16 7.84604 15.8563 7.59031 15.6006C7.33458 15.3449 7.19091 14.998 7.19091 14.6364V14.575C7.18563 14.3493 7.11258 14.1305 6.98126 13.9468C6.84993 13.7632 6.66641 13.6234 6.45455 13.5455C6.2489 13.4547 6.02078 13.4276 5.7996 13.4677C5.57842 13.5078 5.37433 13.6133 5.21364 13.7705L5.17273 13.8114C5.04608 13.9382 4.89569 14.0387 4.73015 14.1074C4.5646 14.176 4.38716 14.2113 4.20795 14.2113C4.02875 14.2113 3.85131 14.176 3.68576 14.1074C3.52022 14.0387 3.36983 13.9382 3.24318 13.8114C3.1164 13.6847 3.01582 13.5343 2.94719 13.3688C2.87857 13.2032 2.84325 13.0258 2.84325 12.8466C2.84325 12.6674 2.87857 12.4899 2.94719 12.3244C3.01582 12.1589 3.1164 12.0085 3.24318 11.8818L3.28409 11.8409C3.44128 11.6802 3.54672 11.4761 3.58682 11.2549C3.62693 11.0338 3.59985 10.8056 3.50909 10.6C3.42266 10.3983 3.27915 10.2264 3.09623 10.1052C2.9133 9.98407 2.69895 9.91906 2.47955 9.91818H2.36364C2.00198 9.91818 1.65513 9.77451 1.3994 9.51878C1.14367 9.26305 1 8.9162 1 8.55455C1 8.19289 1.14367 7.84604 1.3994 7.59031C1.65513 7.33458 2.00198 7.19091 2.36364 7.19091H2.425C2.65068 7.18563 2.86955 7.11258 3.05316 6.98126C3.23677 6.84993 3.37662 6.66641 3.45455 6.45455C3.54531 6.2489 3.57238 6.02078 3.53228 5.7996C3.49217 5.57842 3.38673 5.37433 3.22955 5.21364L3.18864 5.17273C3.06185 5.04608 2.96127 4.89569 2.89265 4.73015C2.82402 4.5646 2.7887 4.38716 2.7887 4.20795C2.7887 4.02875 2.82402 3.85131 2.89265 3.68576C2.96127 3.52022 3.06185 3.36983 3.18864 3.24318C3.31528 3.1164 3.46567 3.01582 3.63122 2.94719C3.79676 2.87857 3.97421 2.84325 4.15341 2.84325C4.33261 2.84325 4.51006 2.87857 4.6756 2.94719C4.84114 3.01582 4.99154 3.1164 5.11818 3.24318L5.15909 3.28409C5.31978 3.44128 5.52387 3.54672 5.74505 3.58682C5.96623 3.62693 6.19435 3.59985 6.4 3.50909H6.45455C6.65621 3.42266 6.82819 3.27915 6.94934 3.09623C7.07048 2.9133 7.13549 2.69895 7.13636 2.47955V2.36364C7.13636 2.00198 7.28003 1.65513 7.53576 1.3994C7.7915 1.14367 8.13834 1 8.5 1C8.86166 1 9.2085 1.14367 9.46424 1.3994C9.71997 1.65513 9.86364 2.00198 9.86364 2.36364V2.425C9.86451 2.6444 9.92952 2.85876 10.0507 3.04168C10.1718 3.22461 10.3438 3.36812 10.5455 3.45455C10.7511 3.54531 10.9792 3.57238 11.2004 3.53228C11.4216 3.49217 11.6257 3.38673 11.7864 3.22955L11.8273 3.18864C11.9539 3.06185 12.1043 2.96127 12.2699 2.89265C12.4354 2.82402 12.6128 2.7887 12.792 2.7887C12.9712 2.7887 13.1487 2.82402 13.3142 2.89265C13.4798 2.96127 13.6302 3.06185 13.7568 3.18864C13.8836 3.31528 13.9842 3.46567 14.0528 3.63122C14.1214 3.79676 14.1568 3.97421 14.1568 4.15341C14.1568 4.33261 14.1214 4.51006 14.0528 4.6756C13.9842 4.84114 13.8836 4.99154 13.7568 5.11818L13.7159 5.15909C13.5587 5.31978 13.4533 5.52387 13.4132 5.74505C13.3731 5.96623 13.4001 6.19435 13.4909 6.4V6.45455C13.5773 6.65621 13.7208 6.82819 13.9038 6.94934C14.0867 7.07048 14.3011 7.13549 14.5205 7.13636H14.6364C14.998 7.13636 15.3449 7.28003 15.6006 7.53576C15.8563 7.7915 16 8.13834 16 8.5C16 8.86166 15.8563 9.2085 15.6006 9.46424C15.3449 9.71997 14.998 9.86364 14.6364 9.86364H14.575C14.3556 9.86451 14.1412 9.92952 13.9583 10.0507C13.7754 10.1718 13.6319 10.3438 13.5455 10.5455V10.5455Z" stroke="#1C0732" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,4 @@
<svg width="17" height="20" viewBox="0 0 17 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.5002 7.17578H7.75044C6.92204 7.17578 6.25049 7.96584 6.25049 8.94043V16.8813C6.25049 17.8559 6.92204 18.646 7.75044 18.646H14.5002C15.3286 18.646 16.0002 17.8559 16.0002 16.8813V8.94043C16.0002 7.96584 15.3286 7.17578 14.5002 7.17578Z" stroke="#1C0732" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.24992 12.4702H2.49995C2.10214 12.4702 1.72062 12.2843 1.43933 11.9534C1.15803 11.6224 1 11.1736 1 10.7056V2.76465C1 2.29663 1.15803 1.84779 1.43933 1.51685C1.72062 1.18592 2.10214 1 2.49995 1H9.24972C9.64754 1 10.0291 1.18592 10.3103 1.51685C10.5916 1.84779 10.7497 2.29663 10.7497 2.76465V3.64697" stroke="#1C0732" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 833 B

View File

@ -0,0 +1,12 @@
<svg width="89" height="89" viewBox="0 0 89 89" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="44.5" cy="44.5" r="44.5" fill="white"/>
<g clip-path="url(#clip0)">
<path d="M17.6912 74.7576H24.4723C24.7355 74.7671 24.9913 74.6685 25.1818 74.4832C25.3723 74.2978 25.4818 74.0419 25.4848 73.7734V71.1675H64.7043V73.7734C64.7077 74.0419 64.8167 74.2978 65.0073 74.4832C65.1978 74.6685 65.4536 74.7671 65.7168 74.7576H72.4979C72.7606 74.7671 73.0155 74.668 73.205 74.4827C73.3941 74.2968 73.5021 74.0414 73.5035 73.7734V66.4275C73.504 65.86 73.0543 65.3997 72.4979 65.3977H70.1096V23.5988H72.4979C73.0543 23.5973 73.504 23.1365 73.5035 22.5695V15.2231C73.5021 14.9556 73.3941 14.6997 73.205 14.5144C73.0155 14.3291 72.7606 14.2299 72.4979 14.2389H65.7168C65.4536 14.2294 65.1978 14.3286 65.0073 14.5139C64.8167 14.6992 64.7077 14.9546 64.7043 15.2231V17.829H25.4848V15.2231C25.4818 14.9546 25.3723 14.6992 25.1818 14.5139C24.9913 14.3286 24.7355 14.2294 24.4723 14.2389H17.6912C17.429 14.2299 17.1741 14.3291 16.9846 14.5144C16.795 14.6997 16.6875 14.9556 16.6855 15.2231V22.5695C16.6855 23.1365 17.1353 23.5973 17.6912 23.5988H20.0795V65.3977H17.6912C17.1353 65.3997 16.6855 65.86 16.6855 66.4275V73.7734C16.6875 74.0414 16.795 74.2968 16.9846 74.4827C17.1741 74.668 17.429 74.7671 17.6912 74.7576ZM71.4923 72.7061H66.7156V67.4492H71.4923V72.7061ZM66.7156 16.2904H71.4923V21.5473H66.7156V16.2904ZM18.6968 16.2904H23.4735V21.5473H18.6968V16.2904ZM22.0908 23.5988H24.4723C25.0301 23.5978 25.4818 23.138 25.4848 22.5695V19.8805H64.7043V22.5695C64.7072 23.138 65.1595 23.5978 65.7168 23.5988H68.0983V65.3977H65.7168C65.1595 65.3992 64.7072 65.859 64.7043 66.4275V69.116H25.4848V66.4275C25.4818 65.859 25.0301 65.3992 24.4723 65.3977H22.0908V23.5988ZM18.6968 67.4492H23.4735V72.7061H18.6968V67.4492Z" fill="#878EDE"/>
<path d="M28.7536 62.1898C28.8297 62.1898 28.9058 62.1808 28.9799 62.1633L40.086 59.5443C40.4499 59.4592 40.7823 59.2714 41.046 59.0019L61.571 38.0233C62.725 36.8413 62.724 34.9296 61.5691 33.7486L55.632 27.6928C54.4737 26.5148 52.5999 26.5138 51.4406 27.6903L30.8738 48.6268C30.6102 48.8953 30.4255 49.2338 30.3416 49.6045L27.774 60.9337C27.7047 61.2382 27.7759 61.5583 27.9665 61.8032C28.1575 62.0476 28.4472 62.1903 28.7536 62.1903V62.1898ZM30.6666 57.278L32.5635 59.2128L30.0961 59.7948L30.6666 57.278ZM31.9674 51.5388L38.1897 57.8855L34.8733 58.6679L31.2004 54.9215L31.9674 51.5388ZM32.838 49.5258L49.3749 32.6918L52.3107 35.6858L35.7905 52.5364L32.838 49.5258ZM40.1651 56.9985L37.213 53.9874L53.7327 37.1363L56.6681 40.1304L40.1651 56.9985ZM52.8612 29.1427C53.2343 28.7641 53.8368 28.7646 54.2095 29.1432L60.1466 35.1995C60.5178 35.5792 60.5183 36.1942 60.1471 36.5744L58.0887 38.6784L50.7979 31.2418L52.8612 29.1427Z" fill="#878EDE"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="59.3333" height="60.52" fill="white" transform="translate(15.4258 14.2383)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1,10 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M4.83033 15.0507H4.83025C4.38898 15.05 3.96735 14.8677 3.66436 14.5467C3.36007 14.2266 3.18686 13.7816 3.18686 13.3092V5.55715C2.59749 5.38143 2.21951 4.80291 2.29952 4.18922L2.29952 4.18921C2.38132 3.56284 2.91496 3.09428 3.54672 3.09415H3.54673H5.11553V2.74908V2.74893H5.16553L4.83033 15.0507ZM4.83033 15.0507H10.8125H10.8126M4.83033 15.0507H10.8126M10.8126 15.0507C11.2539 15.05 11.6755 14.8677 11.9784 14.5467C12.2827 14.2266 12.4559 13.7816 12.4559 13.3092V5.55715C13.0453 5.38143 13.4233 4.80291 13.3433 4.18922L13.3433 4.18921M10.8126 15.0507L13.3433 4.18921M13.3433 4.18921C13.2615 3.56284 12.7278 3.09428 12.0961 3.09415H10.5273L13.3433 4.18921ZM10.1477 1.82725L10.1478 1.82726C10.3925 2.07145 10.5292 2.40337 10.5273 2.74904V2.74893H10.4773L10.5273 2.74921L10.1477 1.82725ZM10.1477 1.82725C9.90304 1.58319 9.57063 1.44727 9.22497 1.45004M10.1477 1.82725L9.22497 1.45004M6.41763 2.13239C6.25287 2.12955 6.09405 2.1934 5.97733 2.30962C5.8605 2.42584 5.79578 2.5843 5.79788 2.74893L6.41763 2.13239ZM6.41763 2.13239H9.22517H6.41763ZM9.22497 1.45004H9.22517V1.50004C9.55752 1.49732 9.87716 1.62799 10.1124 1.86266L9.22497 1.45004ZM9.22497 1.45004H6.41783H6.41763H9.22497ZM10.4773 3.09415V3.14415H10.5273L10.4773 3.09415ZM5.84788 2.74893H5.84788L5.84787 2.7483C5.84595 2.5972 5.90534 2.45176 6.01259 2.34507L6.01261 2.34505C6.11972 2.2384 6.2655 2.17978 6.41677 2.18238V2.18239H6.41763L9.22517 2.1824L9.22603 2.18238C9.3773 2.17978 9.52308 2.2384 9.63019 2.34505L9.63023 2.34509C9.73745 2.45164 9.79685 2.59718 9.79493 2.7483H9.79492V2.74893V3.09415H5.84788V2.74893ZM9.68688 6.34066C9.48463 6.34066 9.32071 6.50459 9.32071 6.70684V12.6826C9.32071 12.8847 9.48462 13.0488 9.68688 13.0488C9.88915 13.0488 10.0531 12.8847 10.0531 12.6826V6.70684C10.0531 6.50459 9.88914 6.34066 9.68688 6.34066ZM5.95594 6.34066C5.75368 6.34066 5.58976 6.50459 5.58976 6.70684V12.6826C5.58976 12.8847 5.75367 13.0488 5.95594 13.0488C6.1582 13.0488 6.32211 12.8847 6.32211 12.6826V6.70684C6.32211 6.50459 6.15819 6.34066 5.95594 6.34066ZM10.8125 14.3184H4.83033C4.32185 14.3184 3.91921 13.8811 3.91921 13.3092V5.59709H11.7236V13.3092C11.7236 13.8811 11.321 14.3184 10.8125 14.3184ZM3.54673 3.8265H12.0961C12.3828 3.8265 12.6152 4.05891 12.6152 4.34562C12.6152 4.63233 12.3828 4.86474 12.0961 4.86474H3.54673C3.26002 4.86474 3.02762 4.63233 3.02762 4.34562C3.02762 4.05891 3.26002 3.8265 3.54673 3.8265ZM7.8214 6.34066C7.61915 6.34066 7.45523 6.50459 7.45523 6.70684V12.6826C7.45523 12.8847 7.61914 13.0488 7.8214 13.0488C8.02367 13.0488 8.18758 12.8847 8.18758 12.6826V6.70684C8.18758 6.50459 8.02365 6.34066 7.8214 6.34066Z" fill="#CA2C2C" stroke="#CA2C2C" stroke-width="0.1"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="15" height="15" fill="white" transform="translate(0.325195 0.75)"/>
</clipPath>
</defs>
<svg width="22" height="24" viewBox="0 0 22 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.52539 5.49609H3.6691H20.8188" stroke="#CA2C2C" stroke-width="1.70732" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.88546 5.49933V3.40299C6.88546 2.847 7.11132 2.31379 7.51334 1.92065C7.91536 1.5275 8.46062 1.30664 9.02916 1.30664H13.3165C13.8851 1.30664 14.4303 1.5275 14.8324 1.92065C15.2344 2.31379 15.4602 2.847 15.4602 3.40299V5.49933M18.6758 5.49933V20.1737C18.6758 20.7297 18.4499 21.2629 18.0479 21.6561C17.6459 22.0492 17.1006 22.2701 16.5321 22.2701H5.81362C5.24507 22.2701 4.69982 22.0492 4.2978 21.6561C3.89577 21.2629 3.66992 20.7297 3.66992 20.1737V5.49933H18.6758Z" stroke="#CA2C2C" stroke-width="1.70732" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.02734 10.7378V17.0268" stroke="#CA2C2C" stroke-width="1.70732" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.3164 10.7378V17.0268" stroke="#CA2C2C" stroke-width="1.70732" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,4 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.928 9.64258C13.6321 9.64258 13.3923 9.88242 13.3923 10.1783V13.3926C13.3923 13.6885 13.1524 13.9283 12.8566 13.9283H2.14233C1.84645 13.9283 1.60661 13.6885 1.60661 13.3926V10.1783C1.60661 9.88242 1.36676 9.64258 1.07088 9.64258C0.775003 9.64258 0.535156 9.88242 0.535156 10.1783V13.3926C0.535156 14.2802 1.2547 14.9997 2.1423 14.9997H12.8566C13.7442 14.9997 14.4637 14.2802 14.4637 13.3926V10.1783C14.4637 9.88242 14.2239 9.64258 13.928 9.64258Z" fill="#1C0732"/>
<path d="M10.5449 9.2641C10.3373 9.06362 10.0082 9.06362 9.80061 9.2641L8.03543 11.0282V0.535724C8.03543 0.239847 7.79558 0 7.49971 0C7.20383 0 6.96398 0.239847 6.96398 0.535724V11.0282L5.19987 9.2641C4.98705 9.05857 4.64792 9.06447 4.44238 9.27726C4.24186 9.48487 4.24186 9.81398 4.44238 10.0216L7.12093 12.7002C7.32989 12.9096 7.66908 12.9101 7.87855 12.7011C7.87886 12.7008 7.87917 12.7005 7.87952 12.7002L10.5581 10.0216C10.7636 9.80877 10.7577 9.46964 10.5449 9.2641Z" fill="#1C0732"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,7 @@
<svg width="12" height="14" viewBox="0 0 12 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.2119 3.40134L7.93069 0.120176C7.8541 0.0435586 7.74956 0 7.64062 0H1.89844C1.21996 0 0.667969 0.551988 0.667969 1.23047V12.7695C0.667969 13.448 1.21996 14 1.89844 14H10.1016C10.78 14 11.332 13.448 11.332 12.7695V3.69141C11.332 3.57957 11.2851 3.47454 11.2119 3.40134ZM8.05078 1.40036L9.93167 3.28125H8.46094C8.23478 3.28125 8.05078 3.09725 8.05078 2.87109V1.40036ZM10.1016 13.1797H1.89844C1.67228 13.1797 1.48828 12.9957 1.48828 12.7695V1.23047C1.48828 1.00431 1.67228 0.820312 1.89844 0.820312H7.23047V2.87109C7.23047 3.54957 7.78246 4.10156 8.46094 4.10156H10.5117V12.7695C10.5117 12.9957 10.3277 13.1797 10.1016 13.1797Z" fill="#1C0732"/>
<path d="M8.46094 5.79688H3.53906C3.31255 5.79688 3.12891 5.98052 3.12891 6.20703C3.12891 6.43355 3.31255 6.61719 3.53906 6.61719H8.46094C8.68745 6.61719 8.87109 6.43355 8.87109 6.20703C8.87109 5.98052 8.68745 5.79688 8.46094 5.79688Z" fill="#1C0732"/>
<path d="M8.46094 7.4375H3.53906C3.31255 7.4375 3.12891 7.62114 3.12891 7.84766C3.12891 8.07417 3.31255 8.25781 3.53906 8.25781H8.46094C8.68745 8.25781 8.87109 8.07417 8.87109 7.84766C8.87109 7.62114 8.68745 7.4375 8.46094 7.4375Z" fill="#1C0732"/>
<path d="M8.46094 9.07812H3.53906C3.31255 9.07812 3.12891 9.26177 3.12891 9.48828C3.12891 9.7148 3.31255 9.89844 3.53906 9.89844H8.46094C8.68745 9.89844 8.87109 9.7148 8.87109 9.48828C8.87109 9.26177 8.68745 9.07812 8.46094 9.07812Z" fill="#1C0732"/>
<path d="M6.82031 10.7188H3.53906C3.31255 10.7188 3.12891 10.9024 3.12891 11.1289C3.12891 11.3554 3.31255 11.5391 3.53906 11.5391H6.82031C7.04683 11.5391 7.23047 11.3554 7.23047 11.1289C7.23047 10.9024 7.04683 10.7188 6.82031 10.7188Z" fill="#1C0732"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,12 @@
<svg width="78" height="75" viewBox="0 0 78 75" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M38.391 1.98234C37.7437 2.01494 37.1095 2.17543 36.5256 2.45438L11.071 14.6169C10.4121 14.9315 9.83325 15.3893 9.37731 15.9563C8.92136 16.5233 8.60006 17.1848 8.43728 17.8919L2.15745 45.2119C2.01244 45.8413 1.99621 46.4933 2.10972 47.129C2.22324 47.7648 2.46419 48.3716 2.81832 48.9134C2.90418 49.0458 2.99653 49.174 3.09502 49.2975L20.7137 71.2037C21.1697 71.7705 21.7487 72.2281 22.4075 72.5426C23.0664 72.8571 23.7882 73.0204 24.5194 73.0202L52.7737 73.0137C53.5046 73.0142 54.2262 72.8515 54.8851 72.5376C55.5439 72.2237 56.123 71.7666 56.5793 71.2004L74.1915 49.291C74.6478 48.7239 74.9693 48.0621 75.1322 47.3547C75.2951 46.6474 75.2951 45.9127 75.1323 45.2054L68.8427 17.8854C68.68 17.1783 68.3587 16.5168 67.9027 15.9498C67.4468 15.3828 66.8679 14.925 66.2091 14.6104L40.7512 2.45438C40.0167 2.10348 39.2051 1.94117 38.391 1.98234Z" fill="#326CE5"/>
<path d="M38.3784 0.00672878C37.6959 0.0424804 37.0274 0.212921 36.4121 0.508074L9.53808 13.3444C8.84261 13.6766 8.23153 14.1599 7.75022 14.7584C7.2689 15.357 6.92974 16.0554 6.7579 16.8018L0.129746 45.6421C-0.0237997 46.3069 -0.0412566 46.9954 0.0784114 47.667C0.198079 48.3386 0.452435 48.9796 0.826413 49.5519C0.91739 49.6911 1.01517 49.8258 1.11939 49.9556L19.718 73.0825C20.1994 73.6808 20.8105 74.1639 21.506 74.4959C22.2015 74.8278 22.9634 75.0001 23.7352 75L53.5619 74.9935C54.3335 74.994 55.0953 74.8222 55.7907 74.4908C56.4862 74.1594 57.0975 73.677 57.5792 73.0792L76.1713 49.9491C76.6525 49.3507 76.9916 48.6525 77.1635 47.9063C77.3354 47.1601 77.3357 46.3851 77.1642 45.6388L70.5263 16.7952C70.354 16.0495 70.0146 15.3517 69.5333 14.7538C69.0521 14.1558 68.4412 13.673 67.7461 13.3412L40.8721 0.504818C40.0961 0.134126 39.2386 -0.037152 38.3784 0.00672878ZM38.3914 1.98606C39.2056 1.94489 40.0172 2.10721 40.7516 2.45811L66.2095 14.6141C66.8683 14.9287 67.4472 15.3865 67.9032 15.9535C68.3591 16.5205 68.6804 17.1821 68.8432 17.8891L75.1328 45.2091C75.2956 45.9165 75.2956 46.6511 75.1327 47.3584C74.9698 48.0658 74.6482 48.7276 74.192 49.2947L56.5798 71.2041C56.1234 71.7704 55.5443 72.2274 54.8855 72.5413C54.2267 72.8552 53.505 73.018 52.7741 73.0174L24.5198 73.0239C23.7886 73.0241 23.0668 72.8609 22.408 72.5464C21.7491 72.2319 21.1702 71.7742 20.7142 71.2074L3.09549 49.3013C2.99699 49.1778 2.90464 49.0496 2.81878 48.9171C2.46465 48.3753 2.2237 47.7686 2.11019 47.1328C1.99667 46.497 2.0129 45.8451 2.15791 45.2156L8.43774 17.8956C8.60052 17.1886 8.92181 16.527 9.37776 15.96C9.83371 15.3931 10.4126 14.9353 11.0714 14.6206L36.526 2.45814C37.1099 2.17919 37.7441 2.0187 38.3914 1.98609L38.3914 1.98606Z" fill="white"/>
<path d="M38.5689 16.4395C37.9761 16.4206 21.4474 24.5473 21.2313 24.9641C20.712 25.9656 16.9504 43.3144 17.1715 43.6883C17.3 43.9054 20.0094 47.3391 23.191 51.3181L28.9758 58.5523L38.495 58.5563L48.0161 58.5603L54.0726 50.993L60.1309 43.4279L58.0105 34.1342C56.8439 29.0225 55.8005 24.7547 55.6939 24.6518C55.3961 24.3652 38.8902 16.45 38.5689 16.4397V16.4395ZM39.4095 20.5012L43.3048 21.6308L39.4095 22.7604L35.5142 21.6308L39.4095 20.5012ZM35.5142 22.0634L39.1618 23.1486L39.1446 28.2198L35.5155 26.2083L35.5142 22.0634ZM43.3048 22.0634V26.2083L39.6758 28.2198L39.6586 23.1486L43.3048 22.0634ZM33.8872 28.9833L37.7826 30.1129L33.8872 31.2425L29.992 30.1129L33.8872 28.9833ZM43.3529 28.9833L47.2483 30.1129L43.3529 31.2425L39.4576 30.1129L43.3529 28.9833ZM29.992 30.5455L33.6395 31.6289L33.6224 36.7L29.9932 34.6904L29.992 30.5455ZM37.7826 30.5455V34.6904L34.1535 36.7L34.1364 31.6289L37.7826 30.5455ZM39.4576 30.5455L43.1052 31.6289L43.088 36.7L39.4589 34.6904L39.4576 30.5455ZM47.2483 30.5455V34.6904L43.6191 36.7L43.602 31.6289L47.2483 30.5455ZM32.4643 37.0383C33.6242 37.0795 32.7262 38.2465 34.0432 38.8723C35.4478 39.5398 35.7377 37.8637 36.6203 39.144C37.5031 40.4244 35.8344 40.1008 35.9585 41.6509C36.0828 43.2011 37.6798 42.6146 37.0123 44.0192C36.3448 45.4238 35.7895 43.815 34.5091 44.6977C33.2288 45.5803 34.5348 46.67 32.9857 46.7942C31.4354 46.9184 32.5507 45.6352 31.1462 44.9676C29.7415 44.3001 29.4517 45.9744 28.569 44.694C27.6864 43.4136 29.3569 43.7391 29.2327 42.1889C29.1084 40.6387 27.5114 41.2253 28.179 39.8207C28.8465 38.4161 29.3999 40.0248 30.6803 39.1422C31.9608 38.2595 30.6546 37.1699 32.2056 37.0457C32.302 37.038 32.3873 37.0358 32.4644 37.038L32.4643 37.0383ZM42.4754 38.5931C44.5468 38.6513 42.5379 40.5954 44.4294 41.4421C46.3213 42.2887 46.4314 39.4959 47.8552 41.0021C49.279 42.5082 46.484 42.4619 47.223 44.3982C47.962 46.3345 50.0156 44.4376 49.9573 46.5095C49.8986 48.5812 47.9549 46.5719 47.1083 48.4636C46.2617 50.3554 49.0545 50.4656 47.5483 51.8894C46.0423 53.3131 46.0903 50.5181 44.154 51.2571C42.2176 51.9961 44.1154 54.0497 42.0427 53.9914C39.9711 53.9332 41.9801 51.9891 40.0886 51.1425C38.1969 50.2958 38.0848 53.0886 36.661 51.5825C35.2373 50.0764 38.0323 50.1245 37.2933 48.1882C36.5543 46.2519 34.5007 48.1487 34.559 46.0769C34.6177 44.0052 36.5613 46.0126 37.408 44.1209C38.2545 42.2292 35.4618 42.1189 36.9679 40.6952C38.4741 39.2714 38.4278 42.0664 40.3641 41.3274C42.3005 40.5884 40.4026 38.5349 42.4754 38.5931ZM32.5549 39.6802C31.2965 39.6806 30.2767 40.7012 30.2773 41.9597C30.2777 43.2175 31.2972 44.237 32.5549 44.2374C33.8134 44.2381 34.8341 43.2182 34.8345 41.9597C34.8351 40.7005 33.8142 39.6795 32.5549 39.6802ZM42.2591 41.7397C39.7439 41.7389 37.7047 43.778 37.7056 46.2932C37.7058 48.8076 39.7447 50.8457 42.2591 50.8448C44.7728 50.8446 46.8105 48.8069 46.8107 46.2932C46.8115 43.7788 44.7735 41.74 42.2591 41.7397Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="77.2926" height="75" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -0,0 +1,14 @@
<svg width="78" height="75" viewBox="0 0 78 75" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M38.391 1.98234C37.7437 2.01494 37.1095 2.17543 36.5256 2.45438L11.071 14.6169C10.4121 14.9315 9.83325 15.3893 9.37731 15.9563C8.92136 16.5233 8.60006 17.1848 8.43728 17.8919L2.15745 45.2119C2.01244 45.8413 1.99621 46.4933 2.10972 47.129C2.22324 47.7648 2.46419 48.3716 2.81832 48.9134C2.90418 49.0458 2.99653 49.174 3.09502 49.2975L20.7137 71.2037C21.1697 71.7705 21.7487 72.2281 22.4075 72.5426C23.0664 72.8571 23.7882 73.0204 24.5194 73.0202L52.7737 73.0137C53.5046 73.0142 54.2262 72.8515 54.8851 72.5376C55.5439 72.2237 56.123 71.7666 56.5793 71.2004L74.1915 49.291C74.6478 48.7239 74.9693 48.0621 75.1322 47.3547C75.2951 46.6474 75.2951 45.9127 75.1323 45.2054L68.8427 17.8854C68.68 17.1783 68.3587 16.5168 67.9027 15.9498C67.4468 15.3828 66.8679 14.925 66.2091 14.6104L40.7512 2.45438C40.0167 2.10348 39.2051 1.94117 38.391 1.98234Z" fill="#326CE5"/>
<path d="M38.3784 0.00672878C37.6959 0.0424804 37.0274 0.212921 36.4121 0.508074L9.53808 13.3444C8.84261 13.6766 8.23153 14.1599 7.75022 14.7584C7.2689 15.357 6.92974 16.0554 6.7579 16.8018L0.129746 45.6421C-0.0237997 46.3069 -0.0412566 46.9954 0.0784114 47.667C0.198079 48.3386 0.452435 48.9796 0.826413 49.5519C0.91739 49.6911 1.01517 49.8258 1.11939 49.9556L19.718 73.0825C20.1994 73.6808 20.8105 74.1639 21.506 74.4959C22.2015 74.8278 22.9634 75.0001 23.7352 75L53.5619 74.9935C54.3335 74.994 55.0953 74.8222 55.7907 74.4908C56.4862 74.1594 57.0975 73.677 57.5792 73.0792L76.1713 49.9491C76.6525 49.3507 76.9916 48.6525 77.1635 47.9063C77.3354 47.1601 77.3357 46.3851 77.1642 45.6388L70.5263 16.7952C70.354 16.0495 70.0146 15.3517 69.5333 14.7538C69.0521 14.1558 68.4412 13.673 67.7461 13.3412L40.8721 0.504818C40.0961 0.134126 39.2386 -0.037152 38.3784 0.00672878ZM38.3914 1.98606C39.2056 1.94489 40.0172 2.10721 40.7516 2.45811L66.2095 14.6141C66.8683 14.9287 67.4472 15.3865 67.9032 15.9535C68.3591 16.5205 68.6804 17.1821 68.8432 17.8891L75.1328 45.2091C75.2956 45.9165 75.2956 46.6511 75.1327 47.3584C74.9698 48.0658 74.6482 48.7276 74.192 49.2947L56.5798 71.2041C56.1234 71.7704 55.5443 72.2274 54.8855 72.5413C54.2267 72.8552 53.505 73.018 52.7741 73.0174L24.5198 73.0239C23.7886 73.0241 23.0668 72.8609 22.408 72.5464C21.7491 72.2319 21.1702 71.7742 20.7142 71.2074L3.09549 49.3013C2.99699 49.1778 2.90464 49.0496 2.81878 48.9171C2.46465 48.3753 2.2237 47.7686 2.11019 47.1328C1.99667 46.497 2.0129 45.8451 2.15791 45.2156L8.43774 17.8956C8.60052 17.1886 8.92181 16.527 9.37776 15.96C9.83371 15.3931 10.4126 14.9353 11.0714 14.6206L36.526 2.45814C37.1099 2.17919 37.7441 2.0187 38.3914 1.98609L38.3914 1.98606Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.1289 26.6303L38.6465 22.1304L54.164 26.6303L38.6465 31.1302L23.1289 26.6303Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.1289 28.3569V44.8685L37.5869 52.8772L37.6585 32.6783L23.1289 28.3569Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M54.1644 28.3569V44.8685L39.7063 52.8772L39.6348 32.6783L54.1644 28.3569Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="77.2926" height="75" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 12H21" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 6H21" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 18H21" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 402 B

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 12H21" stroke="#5B44BA" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 6H21" stroke="#5B44BA" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 18H21" stroke="#5B44BA" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 408 B

View File

@ -0,0 +1,4 @@
<svg width="23" height="23" viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.4793 14.3375C13.0578 14.3375 14.3375 13.0578 14.3375 11.4793C14.3375 9.90075 13.0578 8.62109 11.4793 8.62109C9.90075 8.62109 8.62109 9.90075 8.62109 11.4793C8.62109 13.0578 9.90075 14.3375 11.4793 14.3375Z" stroke="#5B44BA" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.5302 14.3382C18.4034 14.6255 18.3655 14.9443 18.4216 15.2534C18.4776 15.5624 18.6249 15.8476 18.8446 16.0721L18.9017 16.1293C19.0789 16.3063 19.2195 16.5164 19.3153 16.7477C19.4112 16.9791 19.4606 17.227 19.4606 17.4774C19.4606 17.7278 19.4112 17.9758 19.3153 18.2071C19.2195 18.4384 19.0789 18.6486 18.9017 18.8255C18.7248 19.0027 18.5146 19.1432 18.2833 19.2391C18.052 19.335 17.804 19.3844 17.5536 19.3844C17.3032 19.3844 17.0553 19.335 16.824 19.2391C16.5926 19.1432 16.3825 19.0027 16.2055 18.8255L16.1484 18.7684C15.9238 18.5487 15.6386 18.4014 15.3296 18.3453C15.0205 18.2893 14.7018 18.3271 14.4144 18.454C14.1326 18.5747 13.8923 18.7753 13.723 19.0309C13.5537 19.2865 13.4629 19.586 13.4617 19.8926V20.0545C13.4617 20.5599 13.2609 21.0446 12.9036 21.4019C12.5462 21.7592 12.0616 21.96 11.5562 21.96C11.0509 21.96 10.5662 21.7592 10.2089 21.4019C9.85152 21.0446 9.65076 20.5599 9.65076 20.0545V19.9688C9.64339 19.6535 9.54131 19.3476 9.35781 19.0911C9.17431 18.8345 8.91786 18.6391 8.62182 18.5302C8.33446 18.4034 8.0157 18.3655 7.70664 18.4216C7.39758 18.4776 7.11239 18.6249 6.88785 18.8446L6.83069 18.9017C6.65373 19.0789 6.44358 19.2195 6.21226 19.3153C5.98094 19.4112 5.73299 19.4606 5.48258 19.4606C5.23218 19.4606 4.98422 19.4112 4.75291 19.3153C4.52159 19.2195 4.31144 19.0789 4.13447 18.9017C3.95731 18.7248 3.81677 18.5146 3.72088 18.2833C3.62498 18.052 3.57563 17.804 3.57563 17.5536C3.57563 17.3032 3.62498 17.0553 3.72088 16.824C3.81677 16.5926 3.95731 16.3825 4.13447 16.2055L4.19164 16.1484C4.41127 15.9238 4.55861 15.6386 4.61465 15.3296C4.67069 15.0205 4.63286 14.7018 4.50604 14.4144C4.38527 14.1326 4.18473 13.8923 3.92913 13.723C3.67352 13.5537 3.37399 13.4629 3.06742 13.4617H2.90545C2.4001 13.4617 1.91544 13.2609 1.55809 12.9036C1.20075 12.5462 1 12.0616 1 11.5562C1 11.0509 1.20075 10.5662 1.55809 10.2089C1.91544 9.85152 2.4001 9.65076 2.90545 9.65076H2.9912C3.30655 9.64339 3.61238 9.54131 3.86895 9.35781C4.12551 9.17431 4.32094 8.91786 4.42982 8.62182C4.55664 8.33446 4.59447 8.0157 4.53843 7.70664C4.48239 7.39758 4.33506 7.11239 4.11542 6.88785L4.05825 6.83069C3.88109 6.65373 3.74055 6.44358 3.64466 6.21226C3.54877 5.98094 3.49941 5.73299 3.49941 5.48258C3.49941 5.23218 3.54877 4.98422 3.64466 4.75291C3.74055 4.52159 3.88109 4.31144 4.05825 4.13447C4.23522 3.95731 4.44537 3.81677 4.67669 3.72088C4.90801 3.62498 5.15596 3.57563 5.40636 3.57563C5.65677 3.57563 5.90472 3.62498 6.13604 3.72088C6.36736 3.81677 6.57751 3.95731 6.75447 4.13447L6.81164 4.19164C7.03617 4.41127 7.32136 4.55861 7.63042 4.61465C7.93948 4.67069 8.25824 4.63286 8.5456 4.50604H8.62182C8.90361 4.38527 9.14393 4.18473 9.31321 3.92913C9.48248 3.67352 9.57332 3.37399 9.57455 3.06742V2.90545C9.57455 2.4001 9.7753 1.91544 10.1326 1.55809C10.49 1.20075 10.9746 1 11.48 1C11.9854 1 12.47 1.20075 12.8274 1.55809C13.1847 1.91544 13.3855 2.4001 13.3855 2.90545V2.9912C13.3867 3.29777 13.4775 3.5973 13.6468 3.85291C13.8161 4.10852 14.0564 4.30905 14.3382 4.42982C14.6255 4.55664 14.9443 4.59447 15.2534 4.53843C15.5624 4.48239 15.8476 4.33506 16.0721 4.11542L16.1293 4.05825C16.3063 3.88109 16.5164 3.74055 16.7477 3.64466C16.9791 3.54877 17.227 3.49941 17.4774 3.49941C17.7278 3.49941 17.9758 3.54877 18.2071 3.64466C18.4384 3.74055 18.6486 3.88109 18.8255 4.05825C19.0027 4.23522 19.1432 4.44537 19.2391 4.67669C19.335 4.90801 19.3844 5.15596 19.3844 5.40636C19.3844 5.65677 19.335 5.90472 19.2391 6.13604C19.1432 6.36736 19.0027 6.57751 18.8255 6.75447L18.7684 6.81164C18.5487 7.03617 18.4014 7.32136 18.3453 7.63042C18.2893 7.93948 18.3271 8.25824 18.454 8.5456V8.62182C18.5747 8.90361 18.7753 9.14393 19.0309 9.31321C19.2865 9.48248 19.586 9.57332 19.8926 9.57455H20.0545C20.5599 9.57455 21.0446 9.7753 21.4019 10.1326C21.7592 10.49 21.96 10.9746 21.96 11.48C21.96 11.9854 21.7592 12.47 21.4019 12.8274C21.0446 13.1847 20.5599 13.3855 20.0545 13.3855H19.9688C19.6622 13.3867 19.3627 13.4775 19.1071 13.6468C18.8515 13.8161 18.651 14.0564 18.5302 14.3382V14.3382Z" stroke="#5B44BA" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -0,0 +1,11 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M1.80164 8.19168C1.77269 7.95044 1.75781 7.7108 1.75781 7.5C1.75781 4.35345 4.35345 1.75781 7.5 1.75781C8.96771 1.75781 10.3418 2.34959 11.3525 3.27244L10.8457 3.78811C10.7315 3.90232 10.6875 4.06929 10.7315 4.22756C10.7842 4.37691 10.9073 4.49123 11.0654 4.52637C11.1539 4.54411 14.06 5.48058 13.8749 5.44338C14.1103 5.52177 14.4585 5.27069 14.3936 4.92485C14.3757 4.83627 13.7323 1.60801 13.7695 1.79295C13.7344 1.63479 13.6201 1.50295 13.4619 1.45901C13.3126 1.41506 13.1455 1.45901 13.0313 1.57322L12.6006 1.99505C11.2735 0.755882 9.4511 0 7.5 0C3.37795 0 0 3.37795 0 7.5V7.51751C0 7.76527 0.0113297 7.95673 0.0244904 8.10322C0.0419998 8.29731 0.185051 8.45627 0.376167 8.49461L1.27899 8.67508C1.57288 8.7339 1.83712 8.48751 1.80164 8.19168Z" fill="#696F8C"/>
<path d="M14.6271 6.50408L13.7216 6.3196C13.4274 6.25963 13.1613 6.50625 13.1975 6.80346C13.2289 7.05992 13.2428 7.30928 13.2428 7.49937C13.2428 10.6458 10.6472 13.2416 7.50064 13.2416C6.03294 13.2416 4.65884 12.6498 3.6481 11.7181L4.15496 11.2113C4.26917 11.097 4.31312 10.9301 4.26917 10.7718C4.21641 10.6223 4.09339 10.5081 3.93523 10.473C3.84677 10.4552 0.940658 9.51879 1.12571 9.55599C0.976249 9.52967 0.826789 9.57361 0.730201 9.67901C0.624686 9.77571 0.580741 9.93387 0.607062 10.0745C0.624915 10.163 1.26842 13.3619 1.23122 13.177C1.26624 13.3353 1.38057 13.4671 1.53873 13.511C1.72217 13.557 1.8753 13.5013 1.96937 13.3967L2.39131 12.9749C3.71837 14.2142 5.54072 14.9994 7.50064 14.9994C11.6227 14.9994 15.0006 11.6214 15.0006 7.49937V7.47293C15.0006 7.27816 14.9935 7.08693 14.9773 6.89684C14.9606 6.70263 14.8181 6.5431 14.6271 6.50408Z" fill="#696F8C"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="15" height="15" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,3 @@
<svg width="15" height="16" viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.09503 12.362C5.98049 12.4772 5.8242 12.5415 5.66186 12.5415C5.49952 12.5415 5.34324 12.4772 5.22869 12.362L1.76926 8.90199C1.41025 8.54298 1.41025 7.96081 1.76926 7.60247L2.20243 7.16919C2.56156 6.81018 3.14305 6.81018 3.50206 7.16919L5.66186 9.3291L11.498 3.49289C11.8571 3.13388 12.4391 3.13388 12.7976 3.49289L13.2308 3.92618C13.5898 4.28519 13.5898 4.86724 13.2308 5.22569L6.09503 12.362Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 524 B

View File

@ -0,0 +1,3 @@
<svg width="10" height="19" viewBox="0 0 10 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 18L1 9.5L9 1" stroke="#C7D0D9" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 212 B

View File

@ -0,0 +1,3 @@
<svg width="10" height="19" viewBox="0 0 10 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 18L9 9.5L1 1" stroke="#C7D0D9" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 212 B

View File

@ -1,5 +1,6 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect x="0.896484" y="0.71875" width="63" height="63" fill="url(#pattern0)"/>
<svg width="75" height="75" viewBox="0 0 75 75" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<circle cx="37.5" cy="37.5" r="37.5" fill="#E5E7F1"/>
<rect x="11.207" y="11.207" width="52.5862" height="52.5862" fill="url(#pattern0)"/>
<defs>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0" transform="scale(0.0025)"/>

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,5 @@
<svg width="59" height="45" viewBox="0 0 59 45" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.403 10.6179C15.6259 5.62333 21.1938 0.898926 29.5 0.898926C38.4366 0.898926 46.0531 6.44059 47.723 15.4085C50.081 15.7301 52.5497 16.5532 54.5826 17.9294C57.1092 19.6399 59 22.2463 59 25.7265C59 29.1009 57.4412 31.7854 54.9034 33.5849C52.4157 35.3488 49.1049 36.1869 45.6328 36.1869H36.875C35.8567 36.1869 35.0312 35.4346 35.0312 34.5065C35.0312 33.5785 35.8567 32.8261 36.875 32.8261H45.6328C48.4986 32.8261 50.9495 32.1304 52.6391 30.9324C54.2785 29.7699 55.3125 28.0644 55.3125 25.7265C55.3125 23.4945 54.1496 21.8228 52.3837 20.6273C50.5724 19.401 48.1717 18.7228 46.0025 18.6249C45.0943 18.5839 44.3545 17.9455 44.26 17.1212C43.3311 9.02085 36.9431 4.25969 29.5 4.25969C22.4717 4.25969 17.9781 8.48543 16.5057 12.6705C16.2805 13.3106 15.6585 13.7668 14.9244 13.8303C8.57086 14.3804 3.6875 17.9453 3.6875 23.332C3.6875 28.7473 8.82179 32.8261 15.6719 32.8261H22.125C23.1433 32.8261 23.9688 33.5785 23.9688 34.5065C23.9688 35.4346 23.1433 36.1869 22.125 36.1869H15.6719C7.31102 36.1869 0 31.0488 0 23.332C0 15.993 6.37534 11.6084 13.403 10.6179Z" fill="#858CDD"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.1963 14.8345C28.9163 14.1782 30.0837 14.1782 30.8037 14.8345L38.1787 21.556C38.8988 22.2122 38.8988 23.2762 38.1787 23.9324C37.4587 24.5886 36.2913 24.5886 35.5713 23.9324L29.5 18.3991L23.4287 23.9324C22.7087 24.5886 21.5413 24.5886 20.8213 23.9324C20.1012 23.2762 20.1012 22.2122 20.8213 21.556L28.1963 14.8345Z" fill="#858CDD"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.5 16.0225C30.5183 16.0225 31.3438 16.7748 31.3438 17.7028V42.9526C31.3438 43.8807 30.5183 44.633 29.5 44.633C28.4817 44.633 27.6562 43.8807 27.6562 42.9526V17.7028C27.6562 16.7748 28.4817 16.0225 29.5 16.0225Z" fill="#858CDD"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,5 @@
<svg width="89" height="89" viewBox="0 0 89 89" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="44.5" cy="44.5" r="44.5" fill="white"/>
<path d="M48.6049 70.0001V55.2411H59.6741L44.9152 36.7925L30.1562 55.2411H41.2254V70.0001H48.6049Z" fill="#858CDD"/>
<path d="M26.4662 70H33.8457V62.6205H26.4662C20.3634 62.6205 15.397 57.6542 15.397 51.5513C15.397 46.371 19.821 41.3824 25.2597 40.4268L27.4034 40.0504L28.1119 37.9916C30.7057 30.4239 37.148 25.7232 44.9149 25.7232C55.0875 25.7232 63.3636 33.9993 63.3636 44.1719V47.8616H67.0533C71.1231 47.8616 74.4328 51.1713 74.4328 55.2411C74.4328 59.3108 71.1231 62.6205 67.0533 62.6205H55.9841V70H67.0533C75.1928 70 81.8122 63.3806 81.8122 55.2411C81.8122 48.3044 76.9971 42.4672 70.5364 40.8954C68.924 28.1953 58.0503 18.3438 44.9149 18.3438C34.746 18.3438 25.9128 24.2879 21.8799 33.6561C13.9544 36.0249 8.01758 43.5077 8.01758 51.5513C8.01758 61.7239 16.2936 70 26.4662 70Z" fill="#858CDD"/>
</svg>

After

Width:  |  Height:  |  Size: 956 B

View File

@ -231,6 +231,15 @@ analyticsDashboard:
noRecords: No chaos happened during the interval
showTable: Show Chaos during this interval
hideTable: Hide Chaos during this interval
dashboardCloneModal:
heading: Clone the application dashboard
cancel: Cancel
ok: OK
error: Error cloning dashboard
options:
clone: Clone
json: JSON
pdf: PDF
workflowClusterDashboard:
title: Dashboard
tab1: Hourly
@ -261,8 +270,134 @@ analyticsDashboard:
workflowDashboard: Workflow Dashboards
overviewTabdataSourceTable: Connected Data Sources
applicationDashboard: Application Dashboards
applicationDashboards:
createHeader: Create a new dashboard
configureHeader: Configure dashboard
chooseADashboardType:
header: Select a dashboard type
description: Select a dashboard from given types or upload a dashbaords json file for real time monitoring
uploadModal:
heading: Upload a dashboard
option1: Drag & Drop JSON file to upload
or: -- or --
option2: Select JSON file from your device
successMessage: Successfully uploaded
configureDashboardMetadata:
header: Dashboard metadata
description: Provide the dashboard information like names, data source, agents etc.
form:
name: Name
agent: Agent
dataSource: Data source
dashboardType: Dashboard type
applications: Applications
selectNamespaces: Select namespaces
addNamespace: Add namespace
selectApplicationType: Select application type
selectApplications: Select applications
addApplication: Add application
selectTheMetrics:
header: Select metrics
description: Based on the namespaces and application these are the possible metrics that can be plotted.
errorMessage: No panel groups or panels were found. Re-upload the dashboard.
tuneTheQueries:
header: Tune the queries
description: Edit queries to the data source for fetching relevant metrics.
addMetric: + Add Metric
query: Query
queries: Queries
visualization: Visualization
points: Points
grids: Grids
addQuery: Add Query
discardChanges: Discard Changes
metric: Metric
keys: Keys
selectKey: Select key
values: Values
addValue: Add value
seriesName: Series name
label: Label
labelFor: Label for
configuration: Configuration
legend: Legend
legendInfo: Legend
minStep: Min-step
minStepInfo: Min-step
format: Format
formatInfo: Format
timeSeries: Time Series
graph: Graph
graphInfo: Graph
areaGraph: Area graph
lineGraph: Line graph
resolution: Resolution
resolutionInfo: Resolution
stepper:
steps:
step1: Choose a dashboard type
step2: Configure dashboard metadata
step3: Select the metrics
step4: Tune the queries
buttons:
next: Next
back: Back
saveChanges: Save changes
status:
updating: Updating
creating: Creating
errors:
step1:
message: Error loading dashboard
messageViewer: You cannot create a dashboard as a Viewer
step2:
messageViewer: You cannot configure a dashboard as a Viewer
step3:
message: At-least one metric needs to be defined.
step4:
messageCreate: Error creating dashboard
messageConfigure: Error updating dashboard
connectedDataSources: Connected Data Sources
dataSource: Data sources
dataSourceForm:
headingAdd: Add a new data source
headingConfigure: Configure data source
back: Back
step: Step
of2: of 2
saveChanges: Save changes
next: Next
modalHeadingSuccessAdd: A new data source is successfully connected
modalHeadingSuccessConfigure: Data source information is successfully updated
modalHeadingFailed: There was a problem with connecting your data source
modalBodySuccessAdd: You will see the data source connected in the data source table.
modalBodySuccessConfigure: You will see the data source information updated in the data source table.
modalBodyFailed: Try back again or check your entered details.
modalActionsBack: Back to Data Source
modalActionsTryAgain: Try Again
general: General
generalInfo: Provide a name and choose the data source type
name: Name
dataSourceType: Data Source Type
docsAndSetup: Read about Prometheus docs and setup
endPoint: End point
endPointInfo: Provide an URL and access type
url: URL
access: Access
authentication: Authentication
authenticationInfo: Select an authentication option for the data source
noAuth: No auth
basicAuth: Basic auth
withCredentials: With credentials
tlsClientAuth: TLS Client Auth
withCACert: with CA Cert
username: Username
password: Password
configuration: Configuration
configurationInfo: Provide the configuration details
scrapeInterval: Scrape Interval
queryTimeOut: Query timeout
httpMethod: HTTP Method
dataSourceTable:
tableHead1: Status
tableHead2: Data Source Name

View File

@ -13,7 +13,7 @@ interface RangeCallBackType {
}
interface DateRangeSelectorProps {
anchorEl: HTMLElement;
anchorEl: HTMLElement | null;
isOpen: boolean;
onClose: () => void;
callbackToSetRange: RangeCallBackType;
@ -41,7 +41,7 @@ const DateRangeSelector: React.FC<DateRangeSelectorProps> = ({
return (
<div>
<Popover
id={id}
id={id ?? ''}
open={isOpen}
anchorEl={anchorEl}
onClose={onClose}
@ -51,12 +51,12 @@ const DateRangeSelector: React.FC<DateRangeSelectorProps> = ({
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
horizontal: 'left',
}}
classes={{
paper: classes.popoverDateRangeSelector,
}}
className={className}
className={className ?? ''}
>
<div className={classes.dateRangeSelectorContainer}>
<DateRangePicker

View File

@ -18,6 +18,7 @@ interface LitmusStepperProps {
hideNext?: boolean;
disableNext?: boolean;
moreStepperActions?: React.ReactNode;
finishButtonText?: React.ReactNode;
}
const LitmusStepper: React.FC<LitmusStepperProps> = ({
@ -30,6 +31,7 @@ const LitmusStepper: React.FC<LitmusStepperProps> = ({
disableNext,
moreStepperActions,
children,
finishButtonText,
}) => {
const classes = useStyles();
const { t } = useTranslation();
@ -84,18 +86,23 @@ const LitmusStepper: React.FC<LitmusStepperProps> = ({
<div className={classes.endAction}>
{activeStep !== steps.length - 1 ? (
!hideNext && (
<ButtonFilled onClick={handleNext} disabled={disableNext}>
<ButtonFilled
onClick={handleNext}
disabled={disableNext ?? false}
>
<Typography>{t('workflowStepper.next')}</Typography>
</ButtonFilled>
)
) : loader ? (
<ButtonFilled disabled onClick={handleNext}>
{t('workflowStepper.finish')}{' '}
{finishButtonText || t('workflowStepper.finish')}
<span style={{ marginLeft: '0.5rem' }} /> <Loader size={20} />
</ButtonFilled>
) : (
<ButtonFilled onClick={handleNext} disabled={disableNext}>
<Typography>{t('workflowStepper.finish')}</Typography>
<ButtonFilled onClick={handleNext} disabled={disableNext ?? false}>
{finishButtonText || (
<Typography>{t('workflowStepper.finish')}</Typography>
)}
</ButtonFilled>
)}
</div>

View File

@ -1,897 +1,57 @@
import { v4 as uuidv4 } from 'uuid';
export default [
{
dashboardID: 0,
name: 'Kubernetes Platform',
urlToIcon: '/icons/kubernetes-platform.svg',
description: 'System Dashboard',
dashboardTypeID: 'generic_pod_metrics',
typeName: 'Pod metrics',
urlToIcon: '/icons/generic_pod_metrics_dashboard.svg',
information:
'This dashboard visualizes Node and Pod level CPU and memory utilization metrics interleaved with chaos events.',
'This dashboard visualizes Pod level CPU and memory usage metrics interleaved with chaos events.',
urlToDashboard:
'https://raw.githubusercontent.com/litmuschaos/litmus/master/monitoring/portal-dashboards/pod_metrics.json',
chaosEventQueryTemplate:
'litmuschaos_awaited_experiments{chaosresult_name="#{}",chaosresult_namespace="*{}", job="chaos-exporter"}',
panelGroupMap: [
{
groupName: 'CPU Usage Metrics',
panels: ['Chaos-Node-CPU Utilization', 'Chaos-Pod-CPU Usage'],
},
{
groupName: 'Memory Usage Metrics',
panels: ['Chaos-Node-Memory Utilization', 'Chaos-Pod-Memory Usage'],
},
{
groupName: 'Disk Usage Metrics',
panels: [
'Chaos-Node-Disk I/O Usage R/W',
'Chaos-Node-Disk I/O Usage Times',
],
},
{
groupName: 'Network Usage Metrics',
panels: [
'Chaos-Node-Network Traffic Bytes',
'Chaos-Node-Network Traffic Packets',
],
},
],
panelGroups: [
{
panel_group_name: 'CPU Usage Metrics',
panels: [
{
panel_name: 'Chaos-Node-CPU Utilization',
panel_options: {
points: true,
grids: true,
left_axis: true,
},
y_axis_left: 'Cores',
y_axis_right: 'CHAOS',
x_axis_down: 'Time',
unit: '%',
prom_queries: [
{
queryid: uuidv4(),
prom_query_name: 'instance:node_cpu_utilisation:rate1m*100',
legend: '{{instance}}',
resolution: '1/2',
minstep: '5',
line: true,
close_area: false,
},
],
},
{
panel_name: 'Chaos-Pod-CPU Usage',
panel_options: {
points: true,
grids: true,
left_axis: true,
},
y_axis_left: 'Cores',
y_axis_right: 'CHAOS',
x_axis_down: 'Time',
unit: '',
prom_queries: [
{
queryid: uuidv4(),
prom_query_name:
'sum(rate(container_cpu_usage_seconds_total{container!="POD",pod!=""}[5m])) by (pod)',
legend: '{{pod}}',
resolution: '1/2',
minstep: '5',
line: true,
close_area: false,
},
],
},
],
},
{
panel_group_name: 'Memory Usage Metrics',
panels: [
{
panel_name: 'Chaos-Node-Memory Utilization',
panel_options: {
points: true,
grids: true,
left_axis: true,
},
y_axis_left: 'Memory',
y_axis_right: 'CHAOS',
x_axis_down: 'Time',
unit: '%',
prom_queries: [
{
queryid: uuidv4(),
prom_query_name: 'instance:node_memory_utilisation:ratio*100',
legend: '{{instance}}',
resolution: '1/2',
minstep: '5',
line: true,
close_area: false,
},
],
},
{
panel_name: 'Chaos-Pod-Memory Usage',
panel_options: {
points: true,
grids: true,
left_axis: true,
},
y_axis_left: 'Memory',
y_axis_right: 'CHAOS',
x_axis_down: 'Time',
unit: 'GiB',
prom_queries: [
{
queryid: uuidv4(),
prom_query_name:
'sum(container_memory_usage_bytes{container!="POD",container!=""}) by (pod)',
legend: '{{pod}}',
resolution: '1/2',
minstep: '5',
line: true,
close_area: false,
},
],
},
],
},
{
panel_group_name: 'Disk Usage Metrics',
panels: [
{
panel_name: 'Chaos-Node-Disk I/O Usage R/W',
panel_options: {
points: true,
grids: true,
left_axis: true,
},
y_axis_left: 'bytes read (-) / write (+)',
y_axis_right: 'CHAOS',
x_axis_down: 'Time',
unit: 'KiB',
prom_queries: [
{
queryid: uuidv4(),
prom_query_name: 'node_disk_read_bytes_total',
legend: '{{instance}} - {{device}} - Successfully read bytes',
resolution: '1/2',
minstep: '5',
line: true,
close_area: false,
},
{
queryid: uuidv4(),
prom_query_name: 'node_disk_written_bytes_total',
legend:
'{{instance}} - {{device}} - Successfully written bytes',
resolution: '1/2',
minstep: '5',
line: true,
close_area: false,
},
],
},
{
panel_name: 'Chaos-Node-Disk I/O Usage Times',
panel_options: {
points: true,
grids: true,
left_axis: true,
},
y_axis_left: 'time',
y_axis_right: 'CHAOS',
x_axis_down: 'Time',
unit: 'ms',
prom_queries: [
{
queryid: uuidv4(),
prom_query_name: 'node_disk_io_time_seconds_total',
legend: '{{instance}} - {{device}} - Time spent doing I/Os',
resolution: '1/2',
minstep: '5',
line: true,
close_area: false,
},
],
},
],
},
{
panel_group_name: 'Network Usage Metrics',
panels: [
{
panel_name: 'Chaos-Node-Network Traffic Bits',
panel_options: {
points: true,
grids: true,
left_axis: true,
},
y_axis_left: 'bits out (-) / in (+)',
y_axis_right: 'CHAOS',
x_axis_down: 'Time',
unit: 'b/s',
prom_queries: [
{
queryid: uuidv4(),
prom_query_name: 'node_network_receive_bytes_total*8',
legend: '{{instance}} - {{device}} - Receive',
resolution: '1/2',
minstep: '5',
line: true,
close_area: false,
},
{
queryid: uuidv4(),
prom_query_name: 'node_network_transmit_bytes_total*8',
legend: '{{instance}} - {{device}} - Transmit',
resolution: '1/2',
minstep: '5',
line: true,
close_area: false,
},
],
},
{
panel_name: 'Chaos-Node-Network Traffic Packets',
panel_options: {
points: true,
grids: true,
left_axis: true,
},
y_axis_left: 'packets out (-) / in (+)',
y_axis_right: 'CHAOS',
x_axis_down: 'Time',
unit: 'p/s',
prom_queries: [
{
queryid: uuidv4(),
prom_query_name: 'node_network_receive_packets_total',
legend: '{{instance}} - {{device}} - Receive',
resolution: '1/2',
minstep: '5',
line: true,
close_area: false,
},
{
queryid: uuidv4(),
prom_query_name: 'node_network_transmit_packets_total',
legend: '{{instance}} - {{device}} - Transmit',
resolution: '1/2',
minstep: '5',
line: true,
close_area: false,
},
],
},
],
},
],
'litmuschaos_awaited_experiments{job="chaos-exporter"}',
chaosVerdictQueryTemplate:
'litmuschaos_experiment_verdict{job="chaos-exporter"}',
},
{
dashboardID: 1,
name: 'Sock Shop',
urlToIcon: '/icons/sock-shop.svg',
description: 'Application Dashboard',
dashboardTypeID: 'sock-shop',
typeName: 'Sock Shop',
urlToIcon: '/icons/sock-shop_dashboard.svg',
information:
'This dashboard visualizes Sock Shop application metrics metrics interleaved with chaos events and chaos exporter metrics.',
urlToDashboard:
'https://raw.githubusercontent.com/litmuschaos/litmus/master/monitoring/portal-dashboards/sock-shop.json',
chaosEventQueryTemplate:
'litmuschaos_awaited_experiments{chaosresult_name="#{}",chaosresult_namespace="*{}", job="chaos-exporter"}',
panelGroupMap: [
{
groupName: 'Orders Metrics',
panels: ['Orders QPS', 'Orders Latency'],
},
{
groupName: 'Catalogue Metrics',
panels: ['Catalogue QPS', 'Catalogue Latency'],
},
{
groupName: 'Payment Metrics',
panels: ['Payment QPS', 'Payment Latency'],
},
{
groupName: 'Shipping Metrics',
panels: ['Shipping QPS', 'Shipping Latency'],
},
{
groupName: 'User Metrics',
panels: ['User QPS', 'User Latency'],
},
{
groupName: 'Frontend Metrics',
panels: ['Frontend QPS', 'Frontend Latency'],
},
{
groupName: 'Cart Metrics',
panels: ['Cart QPS', 'Cart Latency'],
},
],
panelGroups: [
{
panel_group_name: 'Orders Metrics',
panels: [
{
panel_name: 'Orders QPS',
panel_options: {
points: true,
grids: true,
left_axis: true,
},
y_axis_left: 'QPS (1 min)',
y_axis_right: 'CHAOS',
x_axis_down: 'Time',
unit: 'qps',
prom_queries: [
{
queryid: uuidv4(),
prom_query_name:
'sum(rate(request_duration_seconds_count{job="orders",status_code=~"2..",route!="metrics"}[1m])) * 100',
legend: '2xx',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
{
queryid: uuidv4(),
prom_query_name:
'sum(rate(request_duration_seconds_count{ job="orders", status_code=~"4.+|5.+" }[1m])) * 100',
legend: '4xx/5xx',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
],
},
{
panel_name: 'Orders Latency',
panel_options: {
points: true,
grids: true,
left_axis: true,
},
y_axis_left: 'time',
y_axis_right: 'CHAOS',
x_axis_down: 'Time',
unit: 'ms',
prom_queries: [
{
queryid: uuidv4(),
prom_query_name:
'histogram_quantile(0.99, sum(rate(request_duration_seconds_bucket{job="orders"}[1m])) by (name, le))',
legend: '99th quantile',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
{
queryid: uuidv4(),
prom_query_name:
'histogram_quantile(0.5, sum(rate(request_duration_seconds_bucket{job="orders"}[1m])) by (name, le))',
legend: '50th quantile',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
{
queryid: uuidv4(),
prom_query_name:
'sum(rate(request_duration_seconds_sum{job="orders"}[1m])) / sum(rate(request_duration_seconds_count{job="orders"}[1m]))',
legend: 'Mean',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
],
},
],
},
{
panel_group_name: 'Catalogue Metrics',
panels: [
{
panel_name: 'Catalogue QPS',
panel_options: {
points: true,
grids: true,
left_axis: true,
},
y_axis_left: 'QPS (1 min)',
y_axis_right: 'CHAOS',
x_axis_down: 'Time',
unit: 'qps',
prom_queries: [
{
queryid: uuidv4(),
prom_query_name:
'sum(rate(request_duration_seconds_count{job="catalogue",status_code=~"2..",route!="metrics"}[1m])) * 100',
legend: '2xx',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
{
queryid: uuidv4(),
prom_query_name:
'sum(rate(request_duration_seconds_count{ job="catalogue", status_code=~"4.+|5.+" }[1m])) * 100',
legend: '4xx/5xx',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
],
},
{
panel_name: 'Catalogue Latency',
panel_options: {
points: true,
grids: true,
left_axis: true,
},
y_axis_left: 'time',
y_axis_right: 'CHAOS',
x_axis_down: 'Time',
unit: 'ms',
prom_queries: [
{
queryid: uuidv4(),
prom_query_name:
'histogram_quantile(0.99, sum(rate(request_duration_seconds_bucket{job="catalogue"}[1m])) by (name, le))',
legend: '99th quantile',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
{
queryid: uuidv4(),
prom_query_name:
'histogram_quantile(0.5, sum(rate(request_duration_seconds_bucket{job="catalogue"}[1m])) by (name, le))',
legend: '50th quantile',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
{
queryid: uuidv4(),
prom_query_name:
'sum(rate(request_duration_seconds_sum{job="catalogue"}[1m])) / sum(rate(request_duration_seconds_count{job="catalogue"}[1m]))',
legend: 'Mean',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
],
},
],
},
{
panel_group_name: 'Payment Metrics',
panels: [
{
panel_name: 'Payment QPS',
panel_options: {
points: true,
grids: true,
left_axis: true,
},
y_axis_left: 'QPS (1 min)',
y_axis_right: 'CHAOS',
x_axis_down: 'Time',
unit: 'qps',
prom_queries: [
{
queryid: uuidv4(),
prom_query_name:
'sum(rate(request_duration_seconds_count{job="payment",status_code=~"2..",route!="metrics"}[1m])) * 100',
legend: '2xx',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
{
queryid: uuidv4(),
prom_query_name:
'sum(rate(request_duration_seconds_count{ job="payment", status_code=~"4.+|5.+" }[1m])) * 100',
legend: '4xx/5xx',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
],
},
{
panel_name: 'Payment Latency',
panel_options: {
points: true,
grids: true,
left_axis: true,
},
y_axis_left: 'time',
y_axis_right: 'CHAOS',
x_axis_down: 'Time',
unit: 'ms',
prom_queries: [
{
queryid: uuidv4(),
prom_query_name:
'histogram_quantile(0.99, sum(rate(request_duration_seconds_bucket{job="payment"}[1m])) by (name, le))',
legend: '99th quantile',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
{
queryid: uuidv4(),
prom_query_name:
'histogram_quantile(0.5, sum(rate(request_duration_seconds_bucket{job="payment"}[1m])) by (name, le))',
legend: '50th quantile',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
{
queryid: uuidv4(),
prom_query_name:
'sum(rate(request_duration_seconds_sum{job="payment"}[1m])) / sum(rate(request_duration_seconds_count{job="payment"}[1m]))',
legend: 'Mean',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
],
},
],
},
{
panel_group_name: 'Shipping Metrics',
panels: [
{
panel_name: 'Shipping QPS',
panel_options: {
points: true,
grids: true,
left_axis: true,
},
y_axis_left: 'QPS (1 min)',
y_axis_right: 'CHAOS',
x_axis_down: 'Time',
unit: 'qps',
prom_queries: [
{
queryid: uuidv4(),
prom_query_name:
'sum(rate(request_duration_seconds_count{job="shipping",status_code=~"2..",route!="metrics"}[1m])) * 100',
legend: '2xx',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
{
queryid: uuidv4(),
prom_query_name:
'sum(rate(request_duration_seconds_count{ job="shipping", status_code=~"4.+|5.+" }[1m])) * 100',
legend: '4xx/5xx',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
],
},
{
panel_name: 'Shipping Latency',
panel_options: {
points: true,
grids: true,
left_axis: true,
},
y_axis_left: 'time',
y_axis_right: 'CHAOS',
x_axis_down: 'Time',
unit: 'ms',
prom_queries: [
{
queryid: uuidv4(),
prom_query_name:
'histogram_quantile(0.99, sum(rate(request_duration_seconds_bucket{job="shipping"}[1m])) by (name, le))',
legend: '99th quantile',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
{
queryid: uuidv4(),
prom_query_name:
'histogram_quantile(0.5, sum(rate(request_duration_seconds_bucket{job="shipping"}[1m])) by (name, le))',
legend: '50th quantile',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
{
queryid: uuidv4(),
prom_query_name:
'sum(rate(request_duration_seconds_sum{job="shipping"}[1m])) / sum(rate(request_duration_seconds_count{job="shipping"}[1m]))',
legend: 'Mean',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
],
},
],
},
{
panel_group_name: 'User Metrics',
panels: [
{
panel_name: 'User QPS',
panel_options: {
points: true,
grids: true,
left_axis: true,
},
y_axis_left: 'QPS (1 min)',
y_axis_right: 'CHAOS',
x_axis_down: 'Time',
unit: 'qps',
prom_queries: [
{
queryid: uuidv4(),
prom_query_name:
'sum(rate(request_duration_seconds_count{job="user",status_code=~"2..",route!="metrics"}[1m])) * 100',
legend: '2xx',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
{
queryid: uuidv4(),
prom_query_name:
'sum(rate(request_duration_seconds_count{ job="user", status_code=~"4.+|5.+" }[1m])) * 100',
legend: '4xx/5xx',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
],
},
{
panel_name: 'User Latency',
panel_options: {
points: true,
grids: true,
left_axis: true,
},
y_axis_left: 'time',
y_axis_right: 'CHAOS',
x_axis_down: 'Time',
unit: 'ms',
prom_queries: [
{
queryid: uuidv4(),
prom_query_name:
'histogram_quantile(0.99, sum(rate(request_duration_seconds_bucket{job="user"}[1m])) by (name, le))',
legend: '99th quantile',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
{
queryid: uuidv4(),
prom_query_name:
'histogram_quantile(0.5, sum(rate(request_duration_seconds_bucket{job="user"}[1m])) by (name, le))',
legend: '50th quantile',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
{
queryid: uuidv4(),
prom_query_name:
'sum(rate(request_duration_seconds_sum{job="user"}[1m])) / sum(rate(request_duration_seconds_count{job="user"}[1m]))',
legend: 'Mean',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
],
},
],
},
{
panel_group_name: 'Frontend Metrics',
panels: [
{
panel_name: 'Frontend QPS',
panel_options: {
points: true,
grids: true,
left_axis: true,
},
y_axis_left: 'QPS (1 min)',
y_axis_right: 'CHAOS',
x_axis_down: 'Time',
unit: 'qps',
prom_queries: [
{
queryid: uuidv4(),
prom_query_name:
'sum(rate(request_duration_seconds_count{job="front-end",status_code=~"2..",route!="metrics"}[1m])) * 100',
legend: '2xx',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
{
queryid: uuidv4(),
prom_query_name:
'sum(rate(request_duration_seconds_count{ job="front-end", status_code=~"4.+|5.+" }[1m])) * 100',
legend: '4xx/5xx',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
],
},
{
panel_name: 'Frontend Latency',
panel_options: {
points: true,
grids: true,
left_axis: true,
},
y_axis_left: 'time',
y_axis_right: 'CHAOS',
x_axis_down: 'Time',
unit: 'ms',
prom_queries: [
{
queryid: uuidv4(),
prom_query_name:
'histogram_quantile(0.99, sum(rate(request_duration_seconds_bucket{job="front-end"}[1m])) by (name, le))',
legend: '99th quantile',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
{
queryid: uuidv4(),
prom_query_name:
'histogram_quantile(0.5, sum(rate(request_duration_seconds_bucket{job="front-end"}[1m])) by (name, le))',
legend: '50th quantile',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
{
queryid: uuidv4(),
prom_query_name:
'sum(rate(request_duration_seconds_sum{job="front-end"}[1m])) / sum(rate(request_duration_seconds_count{job="front-end"}[1m]))',
legend: 'Mean',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
],
},
],
},
{
panel_group_name: 'Cart Metrics',
panels: [
{
panel_name: 'Cart QPS',
panel_options: {
points: true,
grids: true,
left_axis: true,
},
y_axis_left: 'QPS (1 min)',
y_axis_right: 'CHAOS',
x_axis_down: 'Time',
unit: 'qps',
prom_queries: [
{
queryid: uuidv4(),
prom_query_name:
'sum(rate(request_duration_seconds_count{job="carts",status_code=~"2..",route!="metrics"}[1m])) * 100',
legend: '2xx',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
{
queryid: uuidv4(),
prom_query_name:
'sum(rate(request_duration_seconds_count{ job="carts", status_code=~"4.+|5.+" }[1m])) * 100',
legend: '4xx/5xx',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
],
},
{
panel_name: 'Cart Latency',
panel_options: {
points: true,
grids: true,
left_axis: true,
},
y_axis_left: 'time',
y_axis_right: 'CHAOS',
x_axis_down: 'Time',
unit: 'ms',
prom_queries: [
{
queryid: uuidv4(),
prom_query_name:
'histogram_quantile(0.99, sum(rate(request_duration_seconds_bucket{job="carts"}[1m])) by (name, le))',
legend: '99th quantile',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
{
queryid: uuidv4(),
prom_query_name:
'histogram_quantile(0.5, sum(rate(request_duration_seconds_bucket{job="carts"}[1m])) by (name, le))',
legend: '50th quantile',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
{
queryid: uuidv4(),
prom_query_name:
'sum(rate(request_duration_seconds_sum{job="carts"}[1m])) / sum(rate(request_duration_seconds_count{job="carts"}[1m]))',
legend: 'Mean',
resolution: '1/2',
minstep: '5',
line: false,
close_area: true,
},
],
},
],
},
],
'litmuschaos_awaited_experiments{job="chaos-exporter"}',
chaosVerdictQueryTemplate:
'litmuschaos_experiment_verdict{job="chaos-exporter"}',
},
{
dashboardTypeID: 'generic_node_metrics',
typeName: 'Node metrics',
urlToIcon: '/icons/generic_node_metrics_dashboard.svg',
information:
'This dashboard visualizes Node level CPU, memory, disk and IO utilization metrics interleaved with chaos events.',
urlToDashboard:
'https://raw.githubusercontent.com/litmuschaos/litmus/master/monitoring/portal-dashboards/node_metrics.json',
chaosEventQueryTemplate:
'litmuschaos_awaited_experiments{job="chaos-exporter"}',
chaosVerdictQueryTemplate:
'litmuschaos_experiment_verdict{job="chaos-exporter"}',
},
{
dashboardTypeID: 'custom',
typeName: 'Custom dashboard',
urlToIcon: '/icons/custom_dashboard.svg',
information: 'Create your own custom dashboard',
chaosEventQueryTemplate:
'litmuschaos_awaited_experiments{job="chaos-exporter"}',
chaosVerdictQueryTemplate:
'litmuschaos_experiment_verdict{job="chaos-exporter"}',
},
{
dashboardTypeID: 'upload',
typeName: 'Upload a dashboard',
urlToIcon: '/icons/upload-json.svg',
information: 'Create a dashboard by uploading a json file',
},
];

View File

@ -0,0 +1,11 @@
import Menu, { MenuProps } from '@material-ui/core/Menu';
import { withStyles } from '@material-ui/core/styles';
import React from 'react';
export const StyledMenu = withStyles({
paper: {
boxShadow:
'0px 0.6px 1.8px rgba(0, 0, 0, 0.1), 2px 3.2px 7.2px rgba(0, 0, 0, 0.13)',
borderRadius: 3,
},
})((props: MenuProps) => <Menu {...props} />);

View File

@ -1,5 +1,5 @@
import { createStyles, Tab, withStyles } from '@material-ui/core';
import React from 'react';
import React, { ReactElement } from 'react';
interface TabPanelProps {
index: number;
@ -19,7 +19,7 @@ const TabPanel: React.FC<TabPanelProps> = ({
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
style={style}
style={style ?? {}}
>
{value === index && children}
</div>
@ -27,6 +27,7 @@ const TabPanel: React.FC<TabPanelProps> = ({
};
interface StyledTabProps {
label: string;
icon?: string | ReactElement;
}
const StyledTab = withStyles((theme) =>
createStyles({
@ -44,6 +45,13 @@ const StyledTab = withStyles((theme) =>
selected: {
color: theme.palette.highlight,
},
wrapper: {
flexDirection: 'row-reverse',
},
labelContainer: {
width: 'auto',
padding: 0,
},
})
)((props: StyledTabProps) => <Tab {...props} />);

View File

@ -23,7 +23,7 @@ export const constants = {
statefulsets: 'statefulsets',
daemonsets: 'daemonsets',
deploymentconfigs: 'deploymentconfigs',
rollouts: 'rollout',
rollouts: 'rollouts',
apps: 'apps',
v1: 'v1',
openshift: 'apps.openshift.io',
@ -34,7 +34,10 @@ export const constants = {
appLabel: 'applabel',
jobCleanUp: 'jobCleanUpPolicy',
nodeselector: 'nodeselector',
pods: 'pods',
services: 'services',
nodes: 'nodes',
replicasets: 'replicasets',
/**
* Template Saved Constants [Component Used in -> Save Template Modal]
*/

View File

@ -26,17 +26,11 @@ const SetNewSchedule = lazy(() => import('../../pages/EditSchedule/Schedule'));
const ConnectTargets = lazy(() => import('../../pages/ConnectTarget'));
const AnalyticsPage = lazy(() => import('../../pages/WorkflowAnalytics'));
const AnalyticsDashboard = lazy(() => import('../../pages/AnalyticsPage'));
const DataSourceSelectPage = lazy(
() => import('../../pages/SelectAndConfigureDataSource/Select')
);
const DataSourceConfigurePage = lazy(
() => import('../../pages/SelectAndConfigureDataSource/Configure')
() => import('../../pages/ConfigureDataSources')
);
const DashboardSelectPage = lazy(
() => import('../../pages/SelectAndConfigureDashboards/Select')
);
const DashboardConfigurePage = lazy(
() => import('../../pages/SelectAndConfigureDashboards/Configure')
const ChooseAndConfigureDashboards = lazy(
() => import('../../pages/ChooseAndConfigureDashboards')
);
const DashboardPage = lazy(() => import('../../pages/ApplicationDashboard'));
const MyHub = lazy(() => import('../../pages/ChaosHub'));
@ -146,11 +140,6 @@ const Routes: React.FC = () => {
<Redirect exact path="/" to="/home" />
<Route exact path="/workflows" component={Workflows} />
<Route exact path="/analytics" component={AnalyticsDashboard} />
<Route
exact
path="/analytics/datasource/select"
component={DataSourceSelectPage}
/>
<Route
exact
path="/analytics/datasource/create"
@ -161,20 +150,15 @@ const Routes: React.FC = () => {
path="/analytics/datasource/configure"
component={() => <DataSourceConfigurePage configure />}
/>
<Route
exact
path="/analytics/dashboard/select"
component={DashboardSelectPage}
/>
<Route
exact
path="/analytics/dashboard/create"
component={() => <DashboardConfigurePage configure={false} />}
component={() => <ChooseAndConfigureDashboards configure={false} />}
/>
<Route
exact
path="/analytics/dashboard/configure"
component={() => <DashboardConfigurePage configure />}
component={() => <ChooseAndConfigureDashboards configure />}
/>
<Route
exact
@ -182,7 +166,6 @@ const Routes: React.FC = () => {
component={() => <DashboardPage />}
/>
<Route exact path="/create-workflow" component={CreateWorkflow} />
<Route
exact
path="/workflows/:workflowRunId"
@ -224,12 +207,10 @@ const Routes: React.FC = () => {
/>
)}
<Route exact path="/404" component={ErrorPage} />
{/* Redirects */}
<Redirect exact path="/getStarted" to="/home" />
<Redirect exact path="/workflows/schedule" to="/workflows" />
<Redirect exact path="/workflows/template" to="/workflows" />
<Redirect exact path="/analytics/overview" to="/analytics" />
<Redirect exact path="/analytics/litmusdashboard" to="/analytics" />
<Redirect

View File

@ -258,13 +258,15 @@ export const DELETE_DATASOURCE = gql`
export const CREATE_DASHBOARD = gql`
mutation createDashBoard($createDBInput: createDBInput) {
createDashBoard(dashboard: $createDBInput)
createDashBoard(dashboard: $createDBInput) {
db_id
}
}
`;
export const UPDATE_DASHBOARD = gql`
mutation updateDashboard($updataDBInput: updataDBInput) {
updateDashboard(dashboard: $updataDBInput)
mutation updateDashboard($updateDBInput: updateDBInput) {
updateDashboard(dashboard: $updateDBInput)
}
`;

View File

@ -383,13 +383,25 @@ export const LIST_DASHBOARD = gql`
db_id
ds_id
db_name
db_type
cluster_name
ds_name
ds_type
db_type_id
db_type_name
db_information
chaos_event_query_template
chaos_verdict_query_template
application_metadata_map {
namespace
applications {
kind
names
}
}
panel_groups {
panels {
panel_id
created_at
prom_queries {
queryid
prom_query_name
@ -429,10 +441,48 @@ export const LIST_DASHBOARD_OVERVIEW = gql`
ListDashboard(project_id: $projectID) {
db_id
db_name
db_type
db_type_id
db_type_name
cluster_name
cluster_id
updated_at
db_information
chaos_event_query_template
chaos_verdict_query_template
application_metadata_map {
namespace
applications {
kind
names
}
}
panel_groups {
panels {
panel_id
created_at
prom_queries {
queryid
prom_query_name
legend
resolution
minstep
line
close_area
}
panel_options {
points
grids
left_axis
}
panel_name
y_axis_left
y_axis_right
x_axis_down
unit
}
panel_group_name
panel_group_id
}
}
}
`;

View File

@ -1,41 +1,92 @@
import { GraphMetric } from 'litmus-ui';
import { PanelGroupResponse, PanelResponse } from './graphql/dashboardsDetails';
import {
ApplicationMetadata,
ListDashboardResponse,
Panel,
PanelGroup,
PanelGroupResponse,
PanelOption,
PanelResponse,
PromQuery,
updatePanelGroupInput,
} from './graphql/dashboardsDetails';
export interface PanelGroupMap {
groupName: string;
panels: string[];
}
export interface PromQueryExport {
prom_query_name: string;
legend: string;
resolution: string;
minstep: string;
line: boolean;
close_area: boolean;
}
export interface PanelExport {
prom_queries: PromQueryExport[];
panel_options: PanelOption;
panel_name: string;
y_axis_left: string;
y_axis_right: string;
x_axis_down: string;
unit: string;
}
export interface PanelGroupExport {
panel_group_name: string;
panels: PanelExport[];
}
export interface DashboardExport {
dashboardID: string;
name: string;
information: string;
chaosEventQueryTemplate: string;
chaosVerdictQueryTemplate: string;
applicationMetadataMap: ApplicationMetadata[];
panelGroupMap: PanelGroupMap[];
panelGroups: PanelGroupExport[];
}
export interface DashboardData {
dashboardID?: number;
name?: string;
urlToIcon?: string;
description?: string;
dashboardTypeID: string;
typeName: string;
urlToIcon: string;
information: string;
urlToDashboard?: string;
chaosEventQueryTemplate?: string;
chaosVerdictQueryTemplate?: string;
handleClick?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
information?: string;
panelGroupMap?: PanelGroupMap[];
}
export interface DashboardDetails {
id: string;
name: string;
dashboardType: string;
dataSourceType: string;
dataSourceID: string;
dataSourceName: string;
agentID: string;
agentName: string;
information: string;
panelGroupMap: PanelGroupMap[];
panelGroups?: PanelGroupResponse[];
id?: string;
name?: string;
dashboardTypeID?: string;
dashboardTypeName?: string;
dataSourceType?: string;
dataSourceID?: string;
dataSourceURL?: string;
chaosEventQueryTemplate?: string;
chaosVerdictQueryTemplate?: string;
agentID?: string;
information?: string;
panelGroups?: PanelGroupDetails[];
panelGroupMap?: updatePanelGroupInput[];
selectedPanelGroupMap?: PanelGroupMap[];
applicationMetadataMap?: ApplicationMetadata[];
selectedPanels?: PanelDetails[];
}
export interface DashboardConfigurationDetails {
name: string;
type: string;
typeID: string;
dataSourceName: string;
dataSourceURL: string;
agent: string;
agentName: string;
}
export interface PanelNameAndID {
@ -54,6 +105,7 @@ export interface GraphPanelGroupProps extends PanelGroupResponse {
export interface ParsedPrometheusData {
seriesData: Array<GraphMetric>;
closedAreaData: Array<GraphMetric>;
chaosData: Array<GraphMetric>;
}
export interface RunWiseChaosMetrics {
@ -85,3 +137,42 @@ export interface ChaosEventDetails {
chaosMetrics: WorkflowAndExperimentMetaDataMap;
showOnTable: Boolean;
}
export interface SelectedDashboardInformation {
id: string;
name: string;
typeName: string;
typeID: string;
agentID: string;
agentName: string;
urlToIcon: string;
information: string;
chaosEventQueryTemplate: string;
chaosVerdictQueryTemplate: string;
applicationMetadataMap: ApplicationMetadata[];
dashboardListForAgent: ListDashboardResponse[];
metaData: ListDashboardResponse[];
dashboardKey: string;
panelNameAndIDList: PanelNameAndID[];
}
export interface PromQueryDetails extends PromQuery {
hidden?: boolean;
base_query?: string;
labels_and_values_list?: QueryLabelValue[];
}
export interface PanelDetails extends Panel {
ds_url?: string;
panel_group_name?: string;
prom_queries: PromQueryDetails[];
}
export interface PanelGroupDetails extends PanelGroup {
panels: PanelDetails[];
}
export interface QueryLabelValue {
label: string;
value: string[];
}

View File

@ -16,7 +16,7 @@ export interface PromQuery {
export interface Panel {
panel_id?: string;
db_id?: string;
created_at?: string;
panel_group_id?: string;
prom_queries: PromQuery[];
panel_options: PanelOption;
@ -35,6 +35,7 @@ export interface PanelGroup {
export interface PanelResponse {
panel_id: string;
created_at: string;
prom_queries: PromQuery[];
panel_options: PanelOption;
panel_name: string;
@ -50,11 +51,26 @@ export interface PanelGroupResponse {
panel_group_id: string;
}
export interface Resource {
kind: string;
names: string[];
}
export interface ApplicationMetadata {
namespace: string;
applications: Resource[];
}
export interface CreateDashboardInput {
createDBInput: {
ds_id: string;
db_name: string;
db_type: string;
db_type_id: string;
db_type_name: string;
db_information: string;
chaos_event_query_template: string;
chaos_verdict_query_template: string;
application_metadata_map: ApplicationMetadata[];
panel_groups: PanelGroup[];
end_time: string;
start_time: string;
@ -62,21 +78,29 @@ export interface CreateDashboardInput {
cluster_id: string;
refresh_rate: string;
};
createDashBoard?: ListDashboardResponse;
}
export interface updatePanelGroupInput {
panel_group_name: string;
panel_group_id: string;
panels: Panel[];
}
export interface UpdateDashboardInput {
updataDBInput: {
updateDBInput: {
db_id: string;
ds_id: string;
db_name: string;
db_type: string;
db_type_id: string;
db_type_name: string;
db_information: string;
chaos_event_query_template: string;
chaos_verdict_query_template: string;
application_metadata_map: ApplicationMetadata[];
end_time: string;
start_time: string;
cluster_id: string;
refresh_rate: string;
panel_groups: updatePanelGroupInput[];
};
@ -90,14 +114,30 @@ export interface UpdatePanelInput {
panelInput: Panel[];
}
export interface ResourceResponse {
kind: string;
names: string[];
}
export interface ApplicationMetadataResponse {
namespace: string;
applications: ResourceResponse[];
}
export interface ListDashboardResponse {
db_id: string;
ds_id: string;
ds_type: string;
db_name: string;
db_type: string;
cluster_name: string;
ds_name: string;
ds_type: string;
db_type_id: string;
db_type_name: string;
db_information: string;
chaos_event_query_template: string;
chaos_verdict_query_template: string;
application_metadata_map: ApplicationMetadataResponse[];
panel_groups: PanelGroupResponse[];
end_time: string;
start_time: string;

View File

@ -48,9 +48,7 @@ export interface PrometheusResponse {
export interface promSeriesInput {
series: string;
url: string;
start: string;
end: string;
ds_details: dsDetails;
}
export interface Option {

View File

@ -1,23 +1,59 @@
export interface PanelGroupMap {
import { ApplicationMetadata } from '../graphql/dashboardsDetails';
interface PanelGroupMetadata {
groupName: string;
panels: string[];
}
interface PromQuery {
prom_query_name: string;
legend: string;
resolution: string;
minstep: string;
line: boolean;
close_area: boolean;
}
interface Panel {
panel_name: string;
panel_options: {
points: boolean;
grids: boolean;
left_axis: boolean;
};
y_axis_left: string;
y_axis_right: string;
x_axis_down: string;
unit: string;
prom_queries: PromQuery[];
}
interface PanelGroup {
panel_group_name: string;
panels: Panel[];
}
export interface ApplicationDashboard {
dashboardID: string;
name: string;
information: string;
chaosEventQueryTemplate: string;
chaosVerdictQueryTemplate: string;
applicationMetadataMap: ApplicationMetadata[];
panelGroupMap: PanelGroupMetadata[];
panelGroups: PanelGroup[];
}
export interface RangeType {
startDate: string;
endDate: string;
}
export interface DashboardData {
dashboardJSON: any;
selectedDashboardID: string;
selectedDashboardName?: string;
selectedDashboardTemplateID?: number;
selectedDashboardTemplateName?: string;
selectedDashboardDescription?: string;
selectedDashboardPanelGroupMap?: PanelGroupMap[];
selectedAgentID?: string;
selectedAgentName?: string;
refreshRate?: number;
activePanelID: string;
refreshRate: number;
range: RangeType;
forceUpdate: Boolean;
}

View File

@ -1,28 +1,16 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useQuery } from '@apollo/client';
import {
FormControl,
IconButton,
InputLabel,
Menu,
MenuItem,
OutlinedInput,
Select,
Typography,
} from '@material-ui/core';
import AutorenewOutlinedIcon from '@material-ui/icons/AutorenewOutlined';
import { IconButton, Menu, MenuItem, Typography } from '@material-ui/core';
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
import WatchLaterRoundedIcon from '@material-ui/icons/WatchLaterRounded';
import { ButtonOutlined } from 'litmus-ui';
import moment from 'moment';
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import BackButton from '../../components/Button/BackButton';
import DateRangeSelector from '../../components/DateRangeSelector';
import Scaffold from '../../containers/layouts/Scaffold';
import { LIST_DASHBOARD, LIST_DATASOURCE } from '../../graphql';
import { PanelNameAndID } from '../../models/dashboardsData';
import {
PanelNameAndID,
SelectedDashboardInformation,
} from '../../models/dashboardsData';
import {
DashboardList,
ListDashboardResponse,
@ -43,38 +31,13 @@ import { getProjectID } from '../../utils/getSearchParams';
import DataSourceInactiveModal from '../../views/Analytics/ApplicationDashboard/DataSourceInactiveModal';
import InfoDropdown from '../../views/Analytics/ApplicationDashboard/InfoDropdown';
import DashboardPanelGroup from '../../views/Analytics/ApplicationDashboard/Panel/DashboardPanelGroup';
import ToolBar from '../../views/Analytics/ApplicationDashboard/ToolBar';
import TopNavButtons from '../../views/Analytics/ApplicationDashboard/TopNavButtons';
import {
ACTIVE,
DEFAULT_REFRESH_RATE,
DEFAULT_TOLERANCE_LIMIT,
MAX_REFRESH_RATE,
MINIMUM_TOLERANCE_LIMIT,
} from './constants';
import refreshData from './refreshData';
import useStyles, { useOutlinedInputStyles } from './styles';
interface SelectedDashboardInformation {
id: string;
name: string;
type: string;
agentID: string;
agentName: string;
dashboardListForAgent: ListDashboardResponse[];
metaData: ListDashboardResponse[];
dashboardKey: string;
panelNameAndIDList: PanelNameAndID[];
}
interface RefreshObjectType {
label: string;
value: number;
}
import { ACTIVE } from './constants';
import useStyles from './styles';
const DashboardPage: React.FC = () => {
const classes = useStyles();
const outlinedInputClasses = useOutlinedInputStyles();
const { t } = useTranslation();
const dataSource = useActions(DataSourceActions);
const dashboard = useActions(DashboardActions);
// get ProjectID
@ -85,22 +48,26 @@ const DashboardPage: React.FC = () => {
const selectedDataSource = useSelector(
(state: RootState) => state.selectDataSource
);
const [selectedDashboardInformation, setSelectedDashboardInformation] =
React.useState<SelectedDashboardInformation>({
id: selectedDashboard.selectedDashboardID ?? '',
name: selectedDashboard.selectedDashboardName ?? '',
type: selectedDashboard.selectedDashboardTemplateName ?? '',
agentID: selectedDashboard.selectedAgentID ?? '',
agentName: selectedDashboard.selectedAgentName ?? '',
name: '',
typeName: '',
typeID: '',
agentID: '',
agentName: '',
urlToIcon: '',
information: '',
chaosEventQueryTemplate: '',
chaosVerdictQueryTemplate: '',
applicationMetadataMap: [],
dashboardListForAgent: [],
metaData: [],
dashboardKey: 'Default',
panelNameAndIDList: [],
});
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const [refreshRate, setRefreshRate] = React.useState<number>(
selectedDashboard.refreshRate ? selectedDashboard.refreshRate : 0
);
const [dataSourceStatus, setDataSourceStatus] =
React.useState<string>('ACTIVE');
const open = Boolean(anchorEl);
@ -110,61 +77,9 @@ const DashboardPage: React.FC = () => {
const handleClose = () => {
setAnchorEl(null);
};
const dateRangeSelectorRef = React.useRef<HTMLButtonElement>(null);
const [isDateRangeSelectorPopoverOpen, setDateRangeSelectorPopoverOpen] =
React.useState(false);
const [isInfoOpen, setIsInfoOpen] = React.useState<Boolean>(false);
const [selectedPanels, setSelectedPanels] = React.useState<string[]>([]);
const clearTimeOuts = async () => {
let id = window.setTimeout(() => {}, 0);
while (id--) {
window.clearTimeout(id);
}
return Promise.resolve(id === 0);
};
const CallbackFromRangeSelector = (
selectedStartDate: string,
selectedEndDate: string
) => {
const startDateFormatted: string = moment(selectedStartDate).format();
const endDateFormatted: string = moment(selectedEndDate)
.add(23, 'hours')
.add(59, 'minutes')
.add(59, 'seconds')
.format();
dashboard.selectDashboard({
range: { startDate: startDateFormatted, endDate: endDateFormatted },
});
const endDate: number =
new Date(moment(endDateFormatted).format()).getTime() / 1000;
const now: number = Math.round(new Date().getTime() / 1000);
const diff: number = Math.abs(now - endDate);
const maxLim: number =
(selectedDashboard.refreshRate ?? DEFAULT_REFRESH_RATE) / 1000 !== 0
? (selectedDashboard.refreshRate ?? DEFAULT_REFRESH_RATE) / 1000 +
MINIMUM_TOLERANCE_LIMIT
: DEFAULT_TOLERANCE_LIMIT;
if (
!(diff >= 0 && diff <= maxLim) &&
selectedDashboard.refreshRate !== MAX_REFRESH_RATE
) {
clearTimeOuts().then(() => {
setRefreshRate(MAX_REFRESH_RATE);
});
}
};
const [openRefresh, setOpenRefresh] = React.useState(false);
const handleCloseRefresh = () => {
setOpenRefresh(false);
};
const handleOpenRefresh = () => {
setOpenRefresh(true);
};
// Apollo query to get the dashboards data
const { data: dashboards } = useQuery<DashboardList, ListDashboardVars>(
LIST_DASHBOARD,
@ -189,41 +104,52 @@ const DashboardPage: React.FC = () => {
selectedDashboardInformation.id !==
selectedDashboardInformation.dashboardKey
) {
const availableDashboards: ListDashboardResponse[] =
dashboards.ListDashboard.filter((data) => {
return data.cluster_id === selectedDashboardInformation.agentID;
});
const selectedDashboard: ListDashboardResponse =
availableDashboards.filter((data) => {
dashboards.ListDashboard.filter((data) => {
return data.db_id === selectedDashboardInformation.id;
})[0];
const selectedPanelNameAndIDList: PanelNameAndID[] = [];
selectedDashboard.panel_groups.forEach(
(panelGroup: PanelGroupResponse) => {
panelGroup.panels.forEach((panel: PanelResponse) => {
selectedPanelNameAndIDList.push({
name: panel.panel_name,
id: panel.panel_id,
if (selectedDashboard) {
(selectedDashboard.panel_groups ?? []).forEach(
(panelGroup: PanelGroupResponse) => {
(panelGroup.panels ?? []).forEach((panel: PanelResponse) => {
selectedPanelNameAndIDList.push({
name: panel.panel_name,
id: panel.panel_id,
});
});
});
}
);
dashboard.selectDashboard({
selectedDashboardID: selectedDashboardInformation.id,
selectedDashboardName: selectedDashboard.db_name,
selectedDashboardTemplateName: selectedDashboard.db_type,
refreshRate: 0,
});
setSelectedDashboardInformation({
...selectedDashboardInformation,
dashboardListForAgent: availableDashboards,
metaData: [selectedDashboard],
dashboardKey: selectedDashboardInformation.id,
panelNameAndIDList: selectedPanelNameAndIDList,
});
setSelectedPanels(
selectedPanelNameAndIDList.map((panel: PanelNameAndID) => panel.id)
);
}
);
}
const availableDashboards: ListDashboardResponse[] = selectedDashboard
? dashboards.ListDashboard.filter((data) => {
return data.cluster_id === selectedDashboard.cluster_id;
})
: [];
if (selectedDashboard) {
setSelectedDashboardInformation({
...selectedDashboardInformation,
dashboardListForAgent: availableDashboards,
metaData: [selectedDashboard],
dashboardKey: selectedDashboardInformation.id,
panelNameAndIDList: selectedPanelNameAndIDList,
name: selectedDashboard.db_name,
typeName: selectedDashboard.db_type_name,
typeID: selectedDashboard.db_type_id,
agentID: selectedDashboard.cluster_id,
agentName: selectedDashboard.cluster_name,
urlToIcon: `/icons/${selectedDashboard.db_type_id}_dashboard.svg`,
information: selectedDashboard.db_information,
chaosEventQueryTemplate:
selectedDashboard.chaos_event_query_template,
chaosVerdictQueryTemplate:
selectedDashboard.chaos_verdict_query_template,
applicationMetadataMap: selectedDashboard.application_metadata_map,
});
setSelectedPanels(
selectedPanelNameAndIDList.map((panel: PanelNameAndID) => panel.id)
);
}
}
}
}, [dashboards, selectedDashboardInformation.id]);
@ -263,29 +189,6 @@ const DashboardPage: React.FC = () => {
}
}, [selectedDashboardInformation.dashboardKey, dataSources]);
const getRefreshRateStatus = () => {
if (selectedDashboard.range) {
const endDate: number =
new Date(moment(selectedDashboard.range.endDate).format()).getTime() /
1000;
const now: number = Math.round(new Date().getTime() / 1000);
const diff: number = Math.abs(now - endDate);
const maxLim: number =
(selectedDashboard.refreshRate ?? DEFAULT_REFRESH_RATE) / 1000 !== 0
? (selectedDashboard.refreshRate ?? DEFAULT_REFRESH_RATE) / 1000 +
MINIMUM_TOLERANCE_LIMIT
: DEFAULT_TOLERANCE_LIMIT;
if (!(diff >= 0 && diff <= maxLim)) {
// A non relative time range has been selected.
// Refresh rate switch is not acknowledged and it's state is locked (Off).
// Select a relative time range or select a different refresh rate to unlock again.
return true;
}
}
// For relative time ranges.
return false;
};
return (
<Scaffold>
<div className={classes.rootContainer}>
@ -334,14 +237,22 @@ const DashboardPage: React.FC = () => {
...selectedDashboardInformation,
id: data.db_id,
name: data.db_name,
type: data.db_type,
typeName: data.db_type_name,
typeID: data.db_type_id,
urlToIcon: `/icons/${data.db_type_id}_dashboard.svg`,
information: data.db_information,
chaosEventQueryTemplate:
data.chaos_event_query_template,
chaosVerdictQueryTemplate:
data.chaos_verdict_query_template,
applicationMetadataMap:
data.application_metadata_map,
});
dataSource.selectDataSource({
selectedDataSourceURL: '',
selectedDataSourceID: '',
selectedDataSourceName: '',
});
setRefreshRate(0);
setAnchorEl(null);
}}
className={
@ -371,175 +282,30 @@ const DashboardPage: React.FC = () => {
switchIsInfoToggled={(toggleState: Boolean) => {
setIsInfoOpen(toggleState);
}}
dashboardData={selectedDashboardInformation}
dashboardTypeID={selectedDashboardInformation.typeID}
/>
</div>
{isInfoOpen && (
<div className={classes.infoDiv}>
<InfoDropdown
dashboardConfigurationDetails={{
name: selectedDashboardInformation.name,
type: selectedDashboardInformation.type,
dataSourceName: selectedDataSource.selectedDataSourceName,
dataSourceURL: selectedDataSource.selectedDataSourceURL,
agent: selectedDashboardInformation.agentName,
}}
metricsToBeShown={
selectedDashboardInformation.panelNameAndIDList
}
applicationsToBeShown={[]}
postPanelSelectionRoutine={(selectedPanelList: string[]) => {
setSelectedPanels(selectedPanelList);
}}
postApplicationSelectionRoutine={(
selectedApplicationList: string[]
) => {}}
/>
</div>
<InfoDropdown
dashboardConfigurationDetails={{
name: selectedDashboardInformation.name,
typeID: selectedDashboardInformation.typeID,
dataSourceName: selectedDataSource.selectedDataSourceName,
dataSourceURL: selectedDataSource.selectedDataSourceURL,
agentName: selectedDashboardInformation.agentName,
}}
metricsToBeShown={selectedDashboardInformation.panelNameAndIDList}
applicationsToBeShown={[]}
postPanelSelectionRoutine={(selectedPanelList: string[]) => {
setSelectedPanels(selectedPanelList);
}}
postApplicationSelectionRoutine={(
selectedApplicationList: string[]
) => {}}
/>
)}
<div className={classes.headerDiv}>
<Typography className={classes.headerInfoText}>
{t('analyticsDashboard.monitoringDashboardPage.headerInfoText')}
</Typography>
<div className={classes.controls}>
<ButtonOutlined
className={classes.selectDate}
onClick={() => setDateRangeSelectorPopoverOpen(true)}
ref={dateRangeSelectorRef}
aria-label="time range"
aria-haspopup="true"
>
<Typography className={classes.displayDate}>
<IconButton className={classes.rangeSelectorClockIcon}>
<WatchLaterRoundedIcon />
</IconButton>
{!selectedDashboard.range ||
selectedDashboard.range.startDate === ' '
? `${t(
'analyticsDashboard.monitoringDashboardPage.rangeSelector.selectPeriod'
)}`
: `${selectedDashboard.range.startDate.split('-')[0]}-${
selectedDashboard.range.startDate.split('-')[1]
}-${selectedDashboard.range.startDate.substring(
selectedDashboard.range.startDate.lastIndexOf('-') + 1,
selectedDashboard.range.startDate.lastIndexOf('T')
)}
${selectedDashboard.range.startDate.substring(
selectedDashboard.range.startDate.lastIndexOf('T') + 1,
selectedDashboard.range.startDate.lastIndexOf('+')
)}
${t(
'analyticsDashboard.monitoringDashboardPage.rangeSelector.to'
)}
${selectedDashboard.range.endDate.split('-')[0]}-${
selectedDashboard.range.endDate.split('-')[1]
}-${selectedDashboard.range.endDate.substring(
selectedDashboard.range.endDate.lastIndexOf('-') + 1,
selectedDashboard.range.endDate.lastIndexOf('T')
)}
${selectedDashboard.range.endDate.substring(
selectedDashboard.range.endDate.lastIndexOf('T') + 1,
selectedDashboard.range.endDate.lastIndexOf('+')
)}`}
<IconButton className={classes.rangeSelectorIcon}>
<KeyboardArrowDownIcon />
</IconButton>
</Typography>
</ButtonOutlined>
<DateRangeSelector
anchorEl={dateRangeSelectorRef.current as HTMLElement}
isOpen={isDateRangeSelectorPopoverOpen}
onClose={() => {
setDateRangeSelectorPopoverOpen(false);
}}
callbackToSetRange={CallbackFromRangeSelector}
className={classes.rangeSelectorPopover}
/>
<FormControl className={classes.formControl} variant="outlined">
<InputLabel
id="refresh-controlled-open-select-label"
className={classes.inputLabel}
>
<AutorenewOutlinedIcon className={classes.refreshIcon} />
<Typography className={classes.refreshText}>
{t(
'analyticsDashboard.monitoringDashboardPage.refresh.heading'
)}
</Typography>
</InputLabel>
<Select
labelId="refresh-controlled-open-select-label"
id="refresh-controlled-open-select"
open={openRefresh}
disabled={getRefreshRateStatus()}
onClose={handleCloseRefresh}
onOpen={handleOpenRefresh}
value={refreshRate !== 0 ? refreshRate : null}
onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
// When viewing data for non-relative time range, refresh should be Off ideally.
// UI can auto detect if it is not Off and switches it to Off.
// Now the user can try to view the non-relative time range data again.
if (selectedDashboard.refreshRate !== MAX_REFRESH_RATE) {
dashboard.selectDashboard({
refreshRate: event.target.value as number,
});
setRefreshRate(event.target.value as number);
} else {
dashboard.selectDashboard({
refreshRate: event.target.value as number,
});
setRefreshRate(event.target.value as number);
dashboard.selectDashboard({
forceUpdate: true,
});
}
}}
input={<OutlinedInput classes={outlinedInputClasses} />}
IconComponent={KeyboardArrowDownIcon}
MenuProps={{
anchorOrigin: {
vertical: 'bottom',
horizontal: 'left',
},
transformOrigin: {
vertical: 'top',
horizontal: 'left',
},
getContentAnchorEl: null,
}}
>
<MenuItem
key="Off-refresh-option"
value={MAX_REFRESH_RATE}
className={
refreshRate === MAX_REFRESH_RATE
? classes.menuListItemSelected
: classes.menuListItem
}
>
{t(
'analyticsDashboard.monitoringDashboardPage.refresh.off'
)}
</MenuItem>
{refreshData.map((data: RefreshObjectType) => (
<MenuItem
key={`${data.label}-refresh-option`}
value={data.value}
className={
refreshRate === data.value
? classes.menuListItemSelected
: classes.menuListItem
}
>
{t(data.label)}
</MenuItem>
))}
</Select>
</FormControl>
</div>
</div>
<ToolBar />
<div
className={classes.analyticsDiv}
key={selectedDashboardInformation.dashboardKey}
@ -552,6 +318,8 @@ const DashboardPage: React.FC = () => {
/>
</div> */}
{selectedDashboardInformation.metaData[0] &&
selectedDashboardInformation.metaData[0].panel_groups.length >
0 &&
selectedDashboardInformation.metaData[0].panel_groups.map(
(panelGroup: PanelGroupResponse) => (
<div
@ -574,9 +342,7 @@ const DashboardPage: React.FC = () => {
{dataSourceStatus !== 'ACTIVE' ? (
<DataSourceInactiveModal
dataSourceStatus={dataSourceStatus}
dashboardType={selectedDashboardInformation.type}
dashboardID={selectedDashboardInformation.id}
dashboardName={selectedDashboardInformation.name}
/>
) : (
<div />

View File

@ -1,4 +1,4 @@
import { fade, makeStyles, Theme } from '@material-ui/core/styles';
import { makeStyles } from '@material-ui/core/styles';
// Component styles
const useStyles = makeStyles((theme) => ({
@ -14,19 +14,6 @@ const useStyles = makeStyles((theme) => ({
marginBottom: theme.spacing(2.5),
},
infoDiv: {
marginTop: theme.spacing(3.5),
},
headerDiv: {
display: 'flex',
justifyContent: 'space-between',
marginTop: theme.spacing(3.5),
paddingTop: theme.spacing(2.15),
backgroundColor: theme.palette.cards.header,
minHeight: '5rem',
},
controlsDiv: {
display: 'flex',
justifyContent: 'space-between',
@ -87,55 +74,6 @@ const useStyles = makeStyles((theme) => ({
fontWeight: 500,
},
selectDate: {
display: 'flex',
height: '2.9rem',
minWidth: '9rem',
marginLeft: theme.spacing(3.75),
border: `0.1px solid ${theme.palette.border.main}`,
background: theme.palette.background.paper,
borderRadius: 4,
textTransform: 'none',
'&:hover': {
borderColor: theme.palette.primary.main,
boxShadow: `0px 4px 5px -2px ${fade(
theme.palette.highlight,
0.2
)},0px 7px 10px 1px ${fade(
theme.palette.highlight,
0.14
)},0px 2px 16px 1px ${fade(theme.palette.highlight, 0.12)}`,
background: theme.palette.background.paper,
},
'&$focused': {
borderColor: theme.palette.primary.main,
background: theme.palette.background.paper,
},
},
displayDate: {
width: '100%',
color: theme.palette.text.primary,
},
rangeSelectorIcon: {
width: '0.625rem',
height: '0.625rem',
marginLeft: theme.spacing(1),
marginRight: theme.spacing(-1),
},
rangeSelectorClockIcon: {
width: '0.825rem',
height: '0.825rem',
marginLeft: theme.spacing(-1),
marginRight: theme.spacing(1),
},
rangeSelectorPopover: {
marginTop: theme.spacing(37.5),
},
formControl: {
width: '9rem',
marginLeft: theme.spacing(1.5),
@ -176,66 +114,9 @@ const useStyles = makeStyles((theme) => ({
},
},
refreshIcon: {
marginRight: theme.spacing(1),
},
refreshText: {
marginTop: theme.spacing(-2.85),
marginLeft: theme.spacing(3.5),
},
headerInfoText: {
fontWeight: 500,
fontSize: '1rem',
lineHeight: '150%',
letterSpacing: '0.02em',
width: '35%',
marginLeft: theme.spacing(2.5),
},
controls: {
marginRight: theme.spacing(2.25),
display: 'flex',
[theme.breakpoints.down('sm')]: {
flexDirection: 'column',
marginRight: 0,
},
},
chaosTableSection: {
padding: theme.spacing(2.5, 2, 0),
},
}));
export const useOutlinedInputStyles = makeStyles((theme: Theme) => ({
root: {
'& $notchedOutline': {
borderColor: theme.palette.border.main,
},
'&:hover $notchedOutline': {
borderColor: theme.palette.primary.main,
boxShadow: `0px 4px 5px -2px ${fade(
theme.palette.highlight,
0.2
)},0px 7px 10px 1px ${fade(
theme.palette.highlight,
0.14
)},0px 2px 16px 1px ${fade(theme.palette.highlight, 0.12)}`,
},
'&$focused $notchedOutline': {
borderColor: theme.palette.primary.main,
background: theme.palette.background.paper,
},
'& .MuiInputLabel-root': {
color: `${theme.palette.text.hint} !important`,
marginTop: `${theme.spacing(2)} !important`,
},
color: theme.palette.text.hint,
background: theme.palette.background.paper,
},
focused: {},
notchedOutline: {},
}));
export default useStyles;

View File

@ -0,0 +1,222 @@
/* eslint-disable no-unused-expressions */
import { useQuery } from '@apollo/client';
import React, { useEffect } from 'react';
import { useSelector } from 'react-redux';
import BackButton from '../../components/Button/BackButton';
import Scaffold from '../../containers/layouts/Scaffold';
import { LIST_DASHBOARD, LIST_DATASOURCE } from '../../graphql';
import {
DashboardDetails,
PanelDetails,
PanelGroupDetails,
PromQueryDetails,
} from '../../models/dashboardsData';
import {
ApplicationMetadata,
ApplicationMetadataResponse,
DashboardList,
ListDashboardResponse,
ListDashboardVars,
PanelGroupResponse,
PanelOption,
PanelResponse,
Resource,
updatePanelGroupInput,
} from '../../models/graphql/dashboardsDetails';
import {
DataSourceList,
ListDataSourceResponse,
ListDataSourceVars,
} from '../../models/graphql/dataSourceDetails';
import useActions from '../../redux/actions';
import * as AlertActions from '../../redux/actions/alert';
import { RootState } from '../../redux/reducers';
import { getProjectID } from '../../utils/getSearchParams';
import DashboardStepper from '../../views/Analytics/ApplicationDashboards/Stepper';
interface ChooseAndConfigureDashboardsProps {
configure: boolean;
}
const ChooseAndConfigureDashboards: React.FC<ChooseAndConfigureDashboardsProps> =
({ configure }) => {
const projectID = getProjectID();
const selectedDashboard = useSelector(
(state: RootState) => state.selectDashboard
);
const alert = useActions(AlertActions);
alert.changeAlertState(false);
// Apollo query to get the data source data
const { data: dataSourceList } = useQuery<
DataSourceList,
ListDataSourceVars
>(LIST_DATASOURCE, {
variables: { projectID },
fetchPolicy: 'cache-and-network',
});
// Apollo query to get the dashboard data
const { data: dashboardList } = useQuery<DashboardList, ListDashboardVars>(
LIST_DASHBOARD,
{
variables: { projectID },
fetchPolicy: 'cache-and-network',
}
);
const [dashboardVars, setDashboardVars] = React.useState<DashboardDetails>({
id: '',
name: '',
dashboardTypeID: '',
dashboardTypeName: '',
dataSourceType: '',
dataSourceID: '',
dataSourceURL: '',
chaosEventQueryTemplate: '',
chaosVerdictQueryTemplate: '',
agentID: '',
information: '',
panelGroupMap: [],
panelGroups: [],
selectedPanelGroupMap: [],
applicationMetadataMap: [],
});
const getExistingPanelGroups = (panelGroupsInput: PanelGroupResponse[]) => {
const panelGroups: PanelGroupDetails[] = [];
if (panelGroupsInput?.length) {
panelGroupsInput.forEach((panelGroup: PanelGroupResponse) => {
const panels: PanelDetails[] = [];
panelGroup.panels.forEach((panel: PanelResponse) => {
const promQueries: PromQueryDetails[] = [];
panel.prom_queries.forEach((promQuery) => {
promQueries.push({
...promQuery,
});
});
const panelOption: PanelOption = {
points: panel.panel_options.points,
grids: panel.panel_options.grids,
left_axis: panel.panel_options.left_axis,
};
panels.push({
...panel,
panel_options: panelOption,
prom_queries: promQueries,
panel_id: panel.panel_id,
created_at: panel.created_at,
panel_group_id: panelGroup.panel_group_id,
panel_group_name: panelGroup.panel_group_name,
});
});
panelGroups.push({
panel_group_id: panelGroup.panel_group_id,
panel_group_name: panelGroup.panel_group_name,
panels,
});
});
}
return panelGroups;
};
const getExistingPanelGroupMap = (
panelGroupsInput: PanelGroupResponse[]
) => {
const panelGroupMap: updatePanelGroupInput[] = [];
if (panelGroupsInput?.length) {
panelGroupsInput.forEach((panelGroup: PanelGroupResponse) => {
panelGroupMap.push({
panel_group_id: panelGroup.panel_group_id,
panel_group_name: panelGroup.panel_group_name,
panels: panelGroup.panels,
});
});
}
return panelGroupMap;
};
const getApplicationMetadataMap = (
applicationMetadataMapResponse: ApplicationMetadataResponse[]
) => {
const applicationMetadataMap: ApplicationMetadata[] = [];
applicationMetadataMapResponse?.forEach((applicationMetadata) => {
const applications: Resource[] = [];
applicationMetadata.applications.forEach((application) => {
applications.push({
kind: application.kind,
names: application.names,
});
});
applicationMetadataMap.push({
namespace: applicationMetadata.namespace,
applications,
});
});
return applicationMetadataMap;
};
const getSelectedDsURL = (selectedDsID: string) => {
const dsList: ListDataSourceResponse[] =
dataSourceList?.ListDataSource ?? [];
let selectedDsURL: string = '';
dsList.forEach((ds) => {
if (ds.ds_id === selectedDsID) {
selectedDsURL = ds.ds_url;
}
});
return selectedDsURL;
};
useEffect(() => {
if (configure === true) {
dashboardList?.ListDashboard?.forEach(
(dashboardDetail: ListDashboardResponse) => {
if (
dashboardDetail.db_id === selectedDashboard.selectedDashboardID
) {
setDashboardVars({
...dashboardVars,
id: selectedDashboard.selectedDashboardID,
name: dashboardDetail.db_name,
dataSourceType: dashboardDetail.ds_type,
dashboardTypeID: dashboardDetail.db_type_id,
dashboardTypeName: dashboardDetail.db_type_name,
dataSourceID: dashboardDetail.ds_id,
dataSourceURL: getSelectedDsURL(dashboardDetail.ds_id),
agentID: dashboardDetail.cluster_id,
information: dashboardDetail.db_information,
panelGroupMap: getExistingPanelGroupMap(
dashboardDetail.panel_groups
),
panelGroups: getExistingPanelGroups(
dashboardDetail.panel_groups
),
chaosEventQueryTemplate:
dashboardDetail.chaos_event_query_template,
chaosVerdictQueryTemplate:
dashboardDetail.chaos_verdict_query_template,
applicationMetadataMap: getApplicationMetadataMap(
dashboardDetail.application_metadata_map
),
});
}
}
);
}
}, [dashboardList, dataSourceList]);
return (
<Scaffold>
<BackButton />
<DashboardStepper
configure={configure}
activePanelID={selectedDashboard.activePanelID}
existingDashboardVars={dashboardVars}
dataSourceList={dataSourceList?.ListDataSource ?? []}
/>
</Scaffold>
);
};
export default ChooseAndConfigureDashboards;

View File

@ -2,7 +2,9 @@ import { useMutation } from '@apollo/client';
import { Typography } from '@material-ui/core';
import { ButtonFilled, ButtonOutlined, Modal } from 'litmus-ui';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import BackButton from '../../components/Button/BackButton';
import Scaffold from '../../containers/layouts/Scaffold';
import { CREATE_DATASOURCE, UPDATE_DATASOURCE } from '../../graphql/mutations';
import { DataSourceDetails } from '../../models/dataSourceData';
@ -26,6 +28,7 @@ const DataSourceConfigurePage: React.FC<DataSourceConfigurePageProps> = ({
configure,
}) => {
const classes = useStyles();
const { t } = useTranslation();
const dataSourceID = useSelector(
(state: RootState) => state.selectDataSource.selectedDataSourceID
);
@ -36,6 +39,7 @@ const DataSourceConfigurePage: React.FC<DataSourceConfigurePageProps> = ({
const projectRole = getProjectRole();
const [open, setOpen] = React.useState(false);
const [disabled, setDisabled] = React.useState(true);
const [page, setPage] = React.useState<number>(1);
const [dataSourceVars, setDataSourceVars] = useState<DataSourceDetails>({
id: '',
name: '',
@ -92,7 +96,10 @@ const DataSourceConfigurePage: React.FC<DataSourceConfigurePageProps> = ({
const dataSourceInput = {
ds_name: dataSourceVars.name,
ds_type: dataSourceVars.dataSourceType,
ds_url: dataSourceVars.url,
ds_url:
dataSourceVars.url[dataSourceVars.url.length - 1] !== '/'
? dataSourceVars.url
: dataSourceVars.url.slice(0, -1),
access_type: dataSourceVars.access,
auth_type: authType,
basic_auth_username: dataSourceVars.username,
@ -116,10 +123,13 @@ const DataSourceConfigurePage: React.FC<DataSourceConfigurePageProps> = ({
authType = 'basic auth';
}
const dataSourceInput = {
ds_id: dataSourceVars.id,
ds_id: dataSourceVars.id ?? '',
ds_name: dataSourceVars.name,
ds_type: dataSourceVars.dataSourceType,
ds_url: dataSourceVars.url,
ds_url:
dataSourceVars.url[dataSourceVars.url.length - 1] !== '/'
? dataSourceVars.url
: dataSourceVars.url.slice(0, -1),
access_type: dataSourceVars.access,
auth_type: authType,
basic_auth_username: dataSourceVars.username,
@ -163,16 +173,15 @@ const DataSourceConfigurePage: React.FC<DataSourceConfigurePageProps> = ({
<div className={classes.rootConfigure}>
{configure === false ? (
<div>
<div className={classes.config}>
<Typography className={classes.heading}>
Configure a new data source
</Typography>
<Typography className={classes.description}>
Connect to a data source for your project.
</Typography>
<div className={classes.backButton}>
<BackButton />
</div>
<Typography className={classes.heading}>
{t('analyticsDashboard.dataSourceForm.headingAdd')}
</Typography>
<ConfigurePrometheus
configure={configure}
page={page}
CallbackToSetVars={(vars: DataSourceDetails) => {
setDataSourceVars(vars);
}}
@ -180,18 +189,18 @@ const DataSourceConfigurePage: React.FC<DataSourceConfigurePageProps> = ({
</div>
) : (
<div>
<div className={classes.config}>
<Typography className={classes.heading}>
Configure data source / {selectedDataSourceName}
</Typography>
<Typography className={classes.description}>
Configure data source information.
</Typography>
<div className={classes.backButton}>
<BackButton />
</div>
<Typography className={classes.heading}>
{t('analyticsDashboard.dataSourceForm.headingConfigure')} /{' '}
{selectedDataSourceName}
</Typography>
{dataSourceID ? (
<ConfigurePrometheus
configure={configure}
dataSourceID={dataSourceID}
page={page}
CallbackToSetVars={(vars: DataSourceDetails) => {
setDataSourceVars(vars);
}}
@ -203,21 +212,36 @@ const DataSourceConfigurePage: React.FC<DataSourceConfigurePageProps> = ({
)}
<div className={classes.buttons}>
<div className={classes.cancelButton}>
<ButtonOutlined
onClick={() => window.history.back()}
disabled={false}
>
<div>Back</div>
{page === 2 && (
<ButtonOutlined onClick={() => setPage(1)} disabled={false}>
<Typography>
{t('analyticsDashboard.dataSourceForm.back')}
</Typography>
</ButtonOutlined>
</div>
)}
<div className={classes.saveButton}>
<Typography className={classes.stepText}>
{t('analyticsDashboard.dataSourceForm.step')}{' '}
<strong>{page}</strong>{' '}
{t('analyticsDashboard.dataSourceForm.of2')}
</Typography>
<ButtonFilled
variant="success"
disabled={disabled}
onClick={() => setMutate(true)}
disabled={
page === 1 && dataSourceVars.name === ''
? true
: page === 2
? disabled
: false
}
onClick={() =>
page === 2 && !disabled ? setMutate(true) : setPage(2)
}
>
<div>{'\u2713'} Save</div>
<Typography>
{page === 2
? `${t('analyticsDashboard.dataSourceForm.saveChanges')}`
: `${t('analyticsDashboard.dataSourceForm.next')}`}
</Typography>
</ButtonFilled>
</div>
</div>
@ -254,10 +278,14 @@ const DataSourceConfigurePage: React.FC<DataSourceConfigurePageProps> = ({
variant="h3"
>
{success === true && configure === false
? `A new data source is successfully connected`
? `${t(
'analyticsDashboard.dataSourceForm.modalHeadingSuccessAdd'
)}`
: success === true && configure === true
? `Data source information is successfully updated`
: `There was a problem with connecting your data source`}
? `${t(
'analyticsDashboard.dataSourceForm.modalHeadingSuccessConfigure'
)}`
: `${t('analyticsDashboard.dataSourceForm.modalHeadingFailed')}`}
</Typography>
<Typography
@ -267,15 +295,18 @@ const DataSourceConfigurePage: React.FC<DataSourceConfigurePageProps> = ({
>
{success === true && configure === false ? (
<div>
You will see the data source connected in the datasource table.
{t('analyticsDashboard.dataSourceForm.modalBodySuccessAdd')}
</div>
) : success === true && configure === true ? (
<div>
You will see the data source information updated in the
datasource table.
{t(
'analyticsDashboard.dataSourceForm.modalBodySuccessConfigure'
)}
</div>
) : (
<div>Try back again or check your entered details.</div>
<div>
{t('analyticsDashboard.dataSourceForm.modalBodyFailed')}
</div>
)}
</Typography>
@ -289,7 +320,9 @@ const DataSourceConfigurePage: React.FC<DataSourceConfigurePageProps> = ({
});
}}
>
<div>Back to Data Source</div>
<div>
{t('analyticsDashboard.dataSourceForm.modalActionsBack')}
</div>
</ButtonFilled>
) : (
<div className={classes.flexButtons}>
@ -300,7 +333,9 @@ const DataSourceConfigurePage: React.FC<DataSourceConfigurePageProps> = ({
}}
disabled={false}
>
<div>Try Again</div>
<div>
{t('analyticsDashboard.dataSourceForm.modalActionsTryAgain')}
</div>
</ButtonOutlined>
<ButtonFilled
@ -312,7 +347,9 @@ const DataSourceConfigurePage: React.FC<DataSourceConfigurePageProps> = ({
});
}}
>
<div>Back to Data Source</div>
<div>
{t('analyticsDashboard.dataSourceForm.modalActionsBack')}
</div>
</ButtonFilled>
</div>
)}

View File

@ -0,0 +1,70 @@
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles((theme) => ({
rootConfigure: {
height: '100%',
width: '100%',
margin: '0 auto',
border: 1,
borderRadius: 3,
paddingBottom: theme.spacing(5),
},
icon: {
width: '6rem',
height: '6rem',
},
backButton: {
marginLeft: theme.spacing(-1),
},
heading: {
fontSize: '2rem',
lineHeight: '2.4375rem',
marginBottom: theme.spacing(4),
},
modal: {
padding: theme.spacing(15, 0),
},
modalHeading: {
fontSize: '2.25rem',
margin: theme.spacing(3, 0, 4.5),
},
modalBody: {
marginBottom: theme.spacing(4.5),
},
closeButton: {
borderColor: theme.palette.border.main,
},
buttons: {
display: 'flex',
justifyContent: 'space-between',
margin: theme.spacing(3, 0, 5),
paddingBottom: theme.spacing(7.5),
},
saveButton: {
display: 'flex',
},
stepText: {
fontSize: '0.8rem',
lineHeight: '140%',
paddingRight: theme.spacing(2.5),
margin: 'auto auto',
},
flexButtons: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-evenly',
},
}));
export default useStyles;

View File

@ -1,344 +0,0 @@
import { useMutation } from '@apollo/client';
import { Typography } from '@material-ui/core';
import { ButtonFilled, ButtonOutlined, Modal } from 'litmus-ui';
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import DashboardList from '../../components/PreconfiguredDashboards/data';
import Scaffold from '../../containers/layouts/Scaffold';
import { CREATE_DASHBOARD, UPDATE_DASHBOARD } from '../../graphql/mutations';
import { DashboardDetails } from '../../models/dashboardsData';
import {
CreateDashboardInput,
Panel,
PanelGroup,
PanelGroupResponse,
UpdateDashboardInput,
updatePanelGroupInput,
} from '../../models/graphql/dashboardsDetails';
import { history } from '../../redux/configureStore';
import { RootState } from '../../redux/reducers';
import { ReactComponent as CrossMarkIcon } from '../../svg/crossmark.svg';
import { getProjectID, getProjectRole } from '../../utils/getSearchParams';
import ConfigureDashboard from '../../views/Analytics/ApplicationDashboards/Form';
import {
DEFAULT_DASHBOARD_REFRESH_RATE_STRING,
DEFAULT_RELATIVE_TIME_RANGE,
} from '../ApplicationDashboard/constants';
import useStyles from './styles';
interface DashboardConfigurePageProps {
configure: boolean;
}
const DashboardConfigurePage: React.FC<DashboardConfigurePageProps> = ({
configure,
}) => {
const classes = useStyles();
const projectID = getProjectID();
const projectRole = getProjectRole();
const dashboardID = useSelector(
(state: RootState) => state.selectDashboard.selectedDashboardID
);
const DashboardTemplateID = useSelector(
(state: RootState) => state.selectDashboard.selectedDashboardTemplateID
);
const selectedDashboardName = useSelector(
(state: RootState) => state.selectDashboard.selectedDashboardName
);
const [open, setOpen] = React.useState(false);
const [disabled, setDisabled] = React.useState(true);
const [dashboardVars, setDashboardVars] = useState<DashboardDetails>({
id: '',
name: '',
dashboardType: '',
dataSourceType: '',
dataSourceID: '',
agentID: '',
dataSourceName: '',
agentName: '',
information: '',
panelGroupMap: [],
panelGroups: [],
});
const [mutate, setMutate] = React.useState(false);
const [success, setSuccess] = React.useState(false);
const [createDashboard] = useMutation<CreateDashboardInput>(
CREATE_DASHBOARD,
{
onCompleted: () => {
setSuccess(true);
setMutate(false);
setOpen(true);
},
onError: () => {
setMutate(false);
setOpen(true);
},
}
);
const [updateDashboard] = useMutation<UpdateDashboardInput>(
UPDATE_DASHBOARD,
{
onCompleted: () => {
setSuccess(true);
setMutate(false);
setOpen(true);
},
onError: () => {
setMutate(false);
setOpen(true);
},
}
);
const getPanelGroups = () => {
if (configure === false) {
const panelGroups: PanelGroup[] = [];
DashboardList[DashboardTemplateID ?? 0].panelGroups.forEach(
(panelGroup) => {
const selectedPanels: Panel[] = [];
panelGroup.panels.forEach((panel) => {
selectedPanels.push(panel);
});
panelGroups.push({
panel_group_name: panelGroup.panel_group_name,
panels: selectedPanels,
});
}
);
return panelGroups;
}
const panelGroups: updatePanelGroupInput[] = [];
if (dashboardVars.panelGroups?.length) {
dashboardVars.panelGroups.forEach((panelGroup: PanelGroupResponse) => {
panelGroups.push({
panel_group_id: panelGroup.panel_group_id,
panel_group_name: panelGroup.panel_group_name,
});
});
}
return panelGroups;
};
const handleCreateMutation = () => {
const dashboardInput = {
ds_id: dashboardVars.dataSourceID,
db_name: dashboardVars.name,
db_type: dashboardVars.dashboardType,
panel_groups: getPanelGroups(),
end_time: `${Math.round(new Date().getTime() / 1000)}`,
start_time: `${
Math.round(new Date().getTime() / 1000) - DEFAULT_RELATIVE_TIME_RANGE
}`,
project_id: projectID,
cluster_id: dashboardVars.agentID,
refresh_rate: DEFAULT_DASHBOARD_REFRESH_RATE_STRING,
};
createDashboard({
variables: { createDBInput: dashboardInput },
});
};
const handleUpdateMutation = () => {
const dashboardInput = {
db_id: dashboardVars.id as string,
ds_id: dashboardVars.dataSourceID,
db_name: dashboardVars.name,
db_type: dashboardVars.dashboardType,
panel_groups: getPanelGroups(),
end_time: `${Math.round(new Date().getTime() / 1000)}`,
start_time: `${
Math.round(new Date().getTime() / 1000) - DEFAULT_RELATIVE_TIME_RANGE
}`,
refresh_rate: DEFAULT_DASHBOARD_REFRESH_RATE_STRING,
};
updateDashboard({
variables: { updataDBInput: dashboardInput },
});
};
useEffect(() => {
if (mutate === true && configure === false) {
handleCreateMutation();
} else if (mutate === true && configure === true) {
handleUpdateMutation();
}
}, [mutate]);
useEffect(() => {
if (
dashboardVars.agentID === '' ||
dashboardVars.dataSourceID === '' ||
dashboardVars.dataSourceType === '' ||
dashboardVars.name === ''
) {
setDisabled(true);
} else {
setDisabled(false);
}
}, [dashboardVars]);
return (
<Scaffold>
<div className={classes.rootConfigure}>
{configure === false ? (
<div>
<div className={classes.config}>
<Typography className={classes.heading}>
Configure a new dashboard
</Typography>
<Typography className={classes.description}>
Provide Dashboard metadata for your project.
</Typography>
</div>
<ConfigureDashboard
configure={configure}
CallbackToSetVars={(vars: DashboardDetails) => {
setDashboardVars(vars);
}}
/>
</div>
) : (
<div>
<div className={classes.config}>
<Typography className={classes.heading}>
Configure dashboard / {selectedDashboardName}
</Typography>
<Typography className={classes.description}>
Configure dashboard information.
</Typography>
</div>
{dashboardID ? (
<ConfigureDashboard
configure={configure}
dashboardID={dashboardID}
CallbackToSetVars={(vars: DashboardDetails) => {
setDashboardVars(vars);
}}
/>
) : (
<div />
)}
</div>
)}
<div className={classes.buttons}>
<div className={classes.cancelButton}>
<ButtonOutlined
onClick={() => window.history.back()}
disabled={false}
>
<div>Back</div>
</ButtonOutlined>
</div>
<div className={classes.saveButton}>
<ButtonFilled
disabled={disabled}
variant="success"
onClick={() => setMutate(true)}
>
<div>{'\u2713'} Save</div>
</ButtonFilled>
</div>
</div>
</div>
<Modal
open={open}
onClose={() => setOpen(false)}
width="60%"
modalActions={
<ButtonOutlined
className={classes.closeButton}
onClick={() => setOpen(false)}
>
&#x2715;
</ButtonOutlined>
}
>
<div className={classes.modal}>
<Typography align="center">
{success === true ? (
<img
src="/icons/finish.svg"
alt="success"
className={classes.icon}
/>
) : (
<CrossMarkIcon className={classes.icon} />
)}
</Typography>
<Typography
className={classes.modalHeading}
align="center"
variant="h3"
>
{success === true && configure === false
? `A new dashboard is successfully created`
: success === true && configure === true
? `Dashboard successfully reconfigured`
: `There was a problem while configuring your dashboard`}
</Typography>
<Typography
align="center"
variant="body1"
className={classes.modalBody}
>
{success === true && configure === false ? (
<div>You will see the dashboard in the home page.</div>
) : success === true && configure === true ? (
<div>
You will see the updated dashboard configuration in the
dashboard table.
</div>
) : (
<div>Try back again or check your entered details.</div>
)}
</Typography>
{success === true ? (
<ButtonFilled
variant="success"
onClick={() => {
history.push({
pathname: '/analytics',
search: `?projectID=${projectID}&projectRole=${projectRole}`,
});
}}
>
<div>Back to Kubernetes Dashboard</div>
</ButtonFilled>
) : (
<div className={classes.flexButtons}>
<ButtonOutlined
onClick={() => {
setOpen(false);
setMutate(true);
}}
disabled={false}
>
<div>Try Again</div>
</ButtonOutlined>
<ButtonFilled
variant="error"
onClick={() => {
history.push({
pathname: '/analytics',
search: `?projectID=${projectID}&projectRole=${projectRole}`,
});
}}
>
<div>Back to Kubernetes Dashboard</div>
</ButtonFilled>
</div>
)}
</div>
</Modal>
</Scaffold>
);
};
export default DashboardConfigurePage;

View File

@ -1,32 +0,0 @@
import { Typography } from '@material-ui/core';
import React from 'react';
import BackButton from '../../components/Button/BackButton';
import DashboardList from '../../components/PreconfiguredDashboards/data';
import Scaffold from '../../containers/layouts/Scaffold';
import DashboardCards from '../../views/Analytics/ApplicationDashboards/Cards/dashBoardCards';
import useStyles from './styles';
const DashboardSelectPage: React.FC = () => {
const classes = useStyles();
return (
<Scaffold>
<div className={classes.root}>
<div className={classes.button}>
<BackButton />
</div>
<Typography className={classes.heading}>
<strong>Select a dashboard</strong>
</Typography>
<Typography className={classes.description}>
Select a dashboard from given types for real time monitoring.
</Typography>
<div className={classes.cards}>
<DashboardCards dashboards={DashboardList} />
</div>
</div>
</Scaffold>
);
};
export default DashboardSelectPage;

View File

@ -1,104 +0,0 @@
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles((theme) => ({
root: {
height: '100%',
width: '80%',
margin: '0 auto',
border: 1,
borderRadius: 3,
paddingBottom: theme.spacing(5),
paddingLeft: theme.spacing(3.125),
paddingRight: theme.spacing(3.125),
overflow: 'hidden',
},
rootConfigure: {
height: '100%',
width: '80%',
margin: '0 auto',
border: 1,
borderRadius: 3,
paddingTop: theme.spacing(-2),
paddingBottom: theme.spacing(5),
paddingLeft: theme.spacing(3.125),
paddingRight: theme.spacing(3.125),
},
icon: {
width: '6rem',
height: '6rem',
},
button: {
marginTop: theme.spacing(5),
marginBottom: theme.spacing(2),
marginLeft: theme.spacing(-2),
},
config: {
marginTop: theme.spacing(5),
marginBottom: theme.spacing(-3),
},
cards: {
background: theme.palette.background.paper,
height: '80%',
},
heading: {
fontSize: '2rem',
fontWeight: 500,
},
modalHeading: {
marginTop: theme.spacing(3.5),
fontSize: '2.25rem',
marginBottom: theme.spacing(4.5),
},
modalBody: {
marginBottom: theme.spacing(4.5),
},
description: {
width: '90%',
marginTop: theme.spacing(1),
marginBottom: theme.spacing(5),
fontSize: '1rem',
},
buttons: {
display: 'flex',
justifyContent: 'space-between',
marginTop: theme.spacing(3),
marginBottom: theme.spacing(5),
paddingBottom: theme.spacing(7.5),
marginLeft: theme.spacing(2),
marginRight: theme.spacing(2),
},
closeButton: {
borderColor: theme.palette.border.main,
},
cancelButton: {
marginLeft: theme.spacing(-2),
},
saveButton: {
marginRight: theme.spacing(-2),
},
flexButtons: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-evenly',
},
modal: {
padding: theme.spacing(15, 0),
},
}));
export default useStyles;

View File

@ -1,32 +0,0 @@
import { Typography } from '@material-ui/core';
import React from 'react';
import BackButton from '../../components/Button/BackButton';
import Scaffold from '../../containers/layouts/Scaffold';
import DataSourceList from '../../views/Analytics/DataSources/Cards/data';
import DataSourceCards from '../../views/Analytics/DataSources/Cards/dataSourceCards';
import useStyles from './styles';
const DataSourceSelectPage: React.FC = () => {
const classes = useStyles();
return (
<Scaffold>
<div className={classes.root}>
<div className={classes.button}>
<BackButton />
</div>
<Typography className={classes.heading}>
<strong>Select a data source</strong>
</Typography>
<Typography className={classes.description}>
Connect or select data source for your agents and go analytics.
</Typography>
<div className={classes.cards}>
<DataSourceCards dataSources={DataSourceList} />
</div>
</div>
</Scaffold>
);
};
export default DataSourceSelectPage;

View File

@ -1,108 +0,0 @@
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles((theme) => ({
root: {
height: '100%',
width: '80%',
margin: '0 auto',
border: 1,
borderRadius: 3,
paddingBottom: theme.spacing(5),
paddingLeft: theme.spacing(3.125),
paddingRight: theme.spacing(3.125),
overflow: 'hidden',
},
rootConfigure: {
height: '100%',
width: '80%',
margin: '0 auto',
border: 1,
borderRadius: 3,
paddingTop: theme.spacing(-2),
paddingBottom: theme.spacing(5),
paddingLeft: theme.spacing(3.125),
paddingRight: theme.spacing(3.125),
},
icon: {
width: '6rem',
height: '6rem',
},
button: {
marginTop: theme.spacing(5),
marginBottom: theme.spacing(2),
marginLeft: theme.spacing(-2),
},
config: {
marginTop: theme.spacing(5),
marginBottom: theme.spacing(-3),
},
cards: {
background: theme.palette.background.paper,
height: '80%',
},
heading: {
fontSize: '2rem',
fontWeight: 500,
},
modal: {
padding: theme.spacing(15, 0),
},
modalHeading: {
marginTop: theme.spacing(3),
fontSize: '2.25rem',
marginBottom: theme.spacing(4.5),
},
modalBody: {
marginBottom: theme.spacing(4.5),
},
closeButton: {
borderColor: theme.palette.border.main,
},
description: {
width: '90%',
marginTop: theme.spacing(1),
marginBottom: theme.spacing(5),
fontSize: '1rem',
},
paddedTop: {
marginTop: theme.spacing(5),
},
buttons: {
display: 'flex',
justifyContent: 'space-between',
marginTop: theme.spacing(3),
marginBottom: theme.spacing(5),
paddingBottom: theme.spacing(7.5),
marginLeft: theme.spacing(2),
marginRight: theme.spacing(2),
},
cancelButton: {
marginLeft: theme.spacing(-2),
},
saveButton: {
marginRight: theme.spacing(-2),
},
flexButtons: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-evenly',
},
}));
export default useStyles;

View File

@ -8,12 +8,14 @@ import createReducer from './createReducer';
const initialState: DashboardData = {
selectedDashboardID: '',
activePanelID: '',
refreshRate: 0,
range: {
startDate: '',
endDate: '',
},
forceUpdate: false,
dashboardJSON: {},
};
export const selectDashboard = createReducer<DashboardData>(initialState, {

View File

@ -0,0 +1,4 @@
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.94727 1.5V15.5" stroke="#1C0732" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M0.947266 8.5H14.9473" stroke="#1C0732" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 329 B

View File

@ -0,0 +1,3 @@
<svg width="8" height="15" viewBox="0 0 8 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.657894 0.671055C0.447369 0.899124 0.447369 1.26779 0.657894 1.49586L6.20017 7.50001L0.657894 13.5041C0.447369 13.7322 0.447369 14.1009 0.657894 14.3289C0.86842 14.557 1.20873 14.557 1.41926 14.3289L7.34223 7.9124C7.44722 7.79866 7.5 7.64934 7.5 7.49999C7.5 7.35064 7.44722 7.20132 7.34223 7.08757L1.41926 0.671027C1.20873 0.442985 0.86842 0.442986 0.657894 0.671055Z" fill="#C7D0D9"/>
</svg>

After

Width:  |  Height:  |  Size: 498 B

View File

@ -0,0 +1,4 @@
<svg width="15" height="18" viewBox="0 0 15 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.4655 6.57568H6.7329C6.02934 6.57568 5.45898 7.2416 5.45898 8.06305V14.7562C5.45898 15.5777 6.02934 16.2436 6.7329 16.2436H12.4655C13.1691 16.2436 13.7395 15.5777 13.7395 14.7562V8.06305C13.7395 7.2416 13.1691 6.57568 12.4655 6.57568Z" stroke="#696F8C" stroke-width="1.70732" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.91098 11.0424H2.27398C1.9361 11.0424 1.61206 10.8857 1.37314 10.6068C1.13422 10.3278 1 9.9495 1 9.55503V2.86188C1 2.4674 1.13422 2.08909 1.37314 1.81015C1.61206 1.53122 1.9361 1.37451 2.27398 1.37451H8.00691C8.34479 1.37451 8.66883 1.53122 8.90775 1.81015C9.14667 2.08909 9.28089 2.4674 9.28089 2.86188V3.60556" stroke="#696F8C" stroke-width="1.70732" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 853 B

View File

@ -0,0 +1,6 @@
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.61719 4.32129H3.27769H16.5617" stroke="#CA2C2C" stroke-width="1.70732" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.76808 4.32378V2.66189C5.76808 2.22113 5.94302 1.79842 6.25442 1.48676C6.56583 1.17509 6.98818 1 7.42857 1H10.7495C11.1899 1 11.6123 1.17509 11.9237 1.48676C12.2351 1.79842 12.41 2.22113 12.41 2.66189V4.32378M14.9008 4.32378V15.957C14.9008 16.3978 14.7258 16.8205 14.4144 17.1321C14.103 17.4438 13.6807 17.6189 13.2403 17.6189H4.93783C4.49744 17.6189 4.07509 17.4438 3.76369 17.1321C3.45229 16.8205 3.27734 16.3978 3.27734 15.957V4.32378H14.9008Z" stroke="#CA2C2C" stroke-width="1.70732" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.42773 8.47607V13.4617" stroke="#CA2C2C" stroke-width="1.70732" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.75 8.47607V13.4617" stroke="#CA2C2C" stroke-width="1.70732" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,4 @@
<svg width="22" height="17" viewBox="0 0 22 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.1543 8.30718C1.1543 8.30718 4.61771 1.37451 10.6787 1.37451C16.7397 1.37451 20.2031 8.30718 20.2031 8.30718C20.2031 8.30718 16.7397 15.2398 10.6787 15.2398C4.61771 15.2398 1.1543 8.30718 1.1543 8.30718Z" stroke="#696F8C" stroke-width="1.70732" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.6776 10.9085C12.1122 10.9085 13.2752 9.74454 13.2752 8.30873C13.2752 6.87293 12.1122 5.70898 10.6776 5.70898C9.24305 5.70898 8.08008 6.87293 8.08008 8.30873C8.08008 9.74454 9.24305 10.9085 10.6776 10.9085Z" stroke="#696F8C" stroke-width="1.70732" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 717 B

View File

@ -0,0 +1,3 @@
<svg width="14" height="7" viewBox="0 0 14 7" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.8289 0.157894C13.6009 -0.0526315 13.2322 -0.0526315 13.0041 0.157894L6.99999 5.70017L0.995859 0.157894C0.76779 -0.0526315 0.399122 -0.0526315 0.171052 0.157894C-0.0570174 0.36842 -0.0570174 0.70873 0.171052 0.919256L6.5876 6.84223C6.70134 6.94722 6.85066 7 7.00001 7C7.14936 7 7.29868 6.94722 7.41243 6.84223L13.829 0.919256C14.057 0.70873 14.057 0.36842 13.8289 0.157894Z" fill="#C7D0D9"/>
</svg>

After

Width:  |  Height:  |  Size: 505 B

View File

@ -0,0 +1,3 @@
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.28602 19C14.3228 19 18.406 14.7467 18.406 9.5C18.406 4.25328 14.3228 0 9.28602 0C4.24918 0 0.166016 4.25328 0.166016 9.5C0.166016 14.7467 4.24918 19 9.28602 19ZM13.846 9.5C13.846 12.1232 11.8044 14.25 9.28602 14.25C6.76759 14.25 4.72602 12.1232 4.72602 9.5C4.72602 6.87683 6.76759 4.75 9.28602 4.75C11.8044 4.75 13.846 6.87683 13.846 9.5ZM16.886 9.5C16.886 13.8723 13.4834 17.4167 9.28602 17.4167C5.08864 17.4167 1.68602 13.8723 1.68602 9.5C1.68602 5.12767 5.08864 1.58333 9.28602 1.58333C13.4834 1.58333 16.886 5.12767 16.886 9.5Z" fill="#5B44BA"/>
</svg>

After

Width:  |  Height:  |  Size: 705 B

View File

@ -1,6 +1,11 @@
/* eslint-disable no-unused-expressions */
import { ParsedPrometheusData } from '../models/dashboardsData';
import { PromQuery } from '../models/graphql/dashboardsDetails';
/* eslint-disable no-useless-escape */
/* eslint-disable no-param-reassign */
import {
ParsedPrometheusData,
PromQueryDetails,
QueryLabelValue,
} from '../models/dashboardsData';
import {
PrometheusResponse,
promQueryInput,
@ -10,13 +15,15 @@ import {
PROMETHEUS_QUERY_RESOLUTION_LIMIT,
} from '../pages/ApplicationDashboard/constants';
const labelMatchOperators = ['==', '!=', '<=', '<', '>=', '>', '=~', '!~', '='];
export const getPromQueryInput = (
prom_queries: PromQuery[],
prom_queries: PromQueryDetails[],
timeRangeDiff: number,
withEvents: Boolean
) => {
const promQueries: promQueryInput[] = [];
prom_queries.forEach((query: PromQuery) => {
prom_queries.forEach((query: PromQueryDetails) => {
promQueries.push({
queryid: query.queryid,
query: query.prom_query_name,
@ -61,10 +68,12 @@ export const getPromQueryInput = (
export const DataParserForPrometheus = (
prometheusData: PrometheusResponse,
lineGraph: string[],
areaGraph: string[]
areaGraph: string[],
closedAreaQueryIDs: string[]
) => {
const parsedPrometheusData: ParsedPrometheusData = {
seriesData: [],
closedAreaData: [],
chaosData: [],
};
prometheusData.GetPromQuery.annotationsResponse?.forEach(
@ -89,20 +98,173 @@ export const DataParserForPrometheus = (
prometheusData.GetPromQuery.metricsResponse?.forEach(
(queryResponse, mainIndex) => {
if (queryResponse && queryResponse.legends && queryResponse.tsvs) {
parsedPrometheusData.seriesData.push(
...queryResponse.legends.map((elem, index) => ({
metricName: elem,
data: queryResponse.tsvs[index].map((dataPoint) => ({
...dataPoint,
})),
baseColor:
lineGraph[
(mainIndex + (index % lineGraph.length)) % lineGraph.length
],
}))
);
if (closedAreaQueryIDs.includes(queryResponse.queryid)) {
parsedPrometheusData.closedAreaData.push(
...queryResponse.legends.map((elem, index) => ({
metricName: elem,
data: queryResponse.tsvs[index].map((dataPoint) => ({
...dataPoint,
})),
baseColor:
areaGraph[
(mainIndex + (index % areaGraph.length)) % areaGraph.length
],
}))
);
} else {
parsedPrometheusData.seriesData.push(
...queryResponse.legends.map((elem, index) => ({
metricName: elem,
data: queryResponse.tsvs[index].map((dataPoint) => ({
...dataPoint,
})),
baseColor:
lineGraph[
(mainIndex + (index % lineGraph.length)) % lineGraph.length
],
}))
);
}
}
}
);
return parsedPrometheusData;
};
export const replaceBetween = (
origin: string,
startIndex: number,
endIndex: number,
insertion: string
) =>
`${origin.substring(0, startIndex)}${insertion}${origin.substring(endIndex)}`;
export const getLabelsAndValues = (queryString: string) => {
const labelValuesList: QueryLabelValue[] = [];
const re = /\{(.*?)\}/g;
const arr: string[] = queryString.match(re) as string[];
if (arr) {
const tempLabelValueList = arr[0].split(',');
tempLabelValueList.forEach((labelValue, index) => {
let adjustedLabelValue = labelValue;
if (index === 0) {
adjustedLabelValue = adjustedLabelValue.substring(1, labelValue.length);
}
if (index === tempLabelValueList.length - 1) {
adjustedLabelValue = adjustedLabelValue.substring(
0,
labelValue.length - 2
);
}
let splitOperator = '';
labelMatchOperators.some((val) => {
const ret = adjustedLabelValue.indexOf(val) !== -1;
if (ret) {
splitOperator = val;
}
return ret;
});
const labelAndValue = adjustedLabelValue.trim().split(splitOperator);
const re1 = /\"(.*?)\"/g;
if (labelAndValue.length > 0 && labelAndValue[1]) {
const arr1: string[] = labelAndValue[1].match(re1) as string[];
if (arr1 && arr1.length > 0) {
let updateStatus = false;
labelValuesList.forEach((labVal) => {
if (labVal.label === labelAndValue[0]) {
labVal.value = labVal.value.concat(
arr1[0].substring(1, arr1[0].length - 1).split('|')
);
updateStatus = true;
}
});
if (!updateStatus) {
labelValuesList.push({
label: labelAndValue[0],
value: arr1[0].substring(1, arr1[0].length - 1).split('|'),
});
}
}
}
});
}
return labelValuesList;
};
export const setLabelsAndValues = (
baseQueryString: string,
queryString: string,
labelValuesList: QueryLabelValue[]
) => {
let existingQueryString: string = queryString;
labelValuesList.forEach((labVal) => {
const matchBracketIndex = existingQueryString.indexOf('{');
let matchLabelIndex = -1;
if (matchBracketIndex !== -1) {
matchLabelIndex = existingQueryString.indexOf(
labVal.label,
matchBracketIndex
);
}
if (matchLabelIndex === -1) {
if (matchBracketIndex === -1) {
const baseConcatIndex =
queryString.indexOf(baseQueryString) + baseQueryString.length - 1;
existingQueryString = `${existingQueryString.slice(
0,
baseConcatIndex + 1
)}{${labVal.label}=~"${labVal.value.join(
'|'
)}"}${existingQueryString.slice(baseConcatIndex + 1)}`;
} else {
existingQueryString = `${existingQueryString.slice(
0,
matchBracketIndex + 1
)}${labVal.label}=~"${labVal.value.join(
'|'
)}",${existingQueryString.slice(matchBracketIndex + 1)}`;
}
} else {
const lastIndexOfOpr = existingQueryString.indexOf(`"`, matchLabelIndex);
const lastIndexOfVal = existingQueryString.indexOf(
`"`,
lastIndexOfOpr + 1
);
const subStrToReplace = existingQueryString.substring(
lastIndexOfOpr,
lastIndexOfVal + 1
);
if (lastIndexOfOpr !== -1 && lastIndexOfVal !== -1) {
if (labVal.value.length) {
existingQueryString = existingQueryString.replace(
subStrToReplace,
`"${labVal.value.join('|')}"`
);
existingQueryString = replaceBetween(
existingQueryString,
matchLabelIndex + labVal.label.length,
lastIndexOfOpr,
'=~'
);
} else {
const graceIndexForBrackets =
(existingQueryString[lastIndexOfVal + 1] === '}' ||
existingQueryString[lastIndexOfVal + 2] === '}') &&
existingQueryString[matchLabelIndex - 1] === '{'
? 1
: 0;
existingQueryString = existingQueryString.replace(
existingQueryString.substring(
matchLabelIndex - graceIndexForBrackets,
existingQueryString[lastIndexOfVal + 1] === ','
? lastIndexOfVal + 2 + graceIndexForBrackets
: lastIndexOfVal + 1 + graceIndexForBrackets
),
``
);
}
}
}
});
return existingQueryString;
};

View File

@ -0,0 +1,237 @@
/* eslint-disable no-unused-expressions */
import { useMutation } from '@apollo/client';
import { Snackbar, Typography } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import { ButtonFilled, ButtonOutlined, InputField, Modal } from 'litmus-ui';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { v4 as uuidv4 } from 'uuid';
import { CREATE_DASHBOARD } from '../../../../graphql/mutations';
import { SelectedDashboardInformation } from '../../../../models/dashboardsData';
import {
ApplicationMetadata,
CreateDashboardInput,
Panel,
PanelGroup,
PanelOption,
PromQuery,
Resource,
} from '../../../../models/graphql/dashboardsDetails';
import {
DEFAULT_DASHBOARD_REFRESH_RATE_STRING,
DEFAULT_RELATIVE_TIME_RANGE,
} from '../../../../pages/ApplicationDashboard/constants';
import useActions from '../../../../redux/actions';
import * as DashboardActions from '../../../../redux/actions/dashboards';
import * as DataSourceActions from '../../../../redux/actions/dataSource';
import { getProjectID } from '../../../../utils/getSearchParams';
import { validateTextEmpty } from '../../../../utils/validate';
import useStyles from './styles';
interface DashboardCloneModalProps {
dashboardData: SelectedDashboardInformation;
onClose: () => void;
}
const DashboardCloneModal: React.FC<DashboardCloneModalProps> = ({
dashboardData,
onClose,
}) => {
const classes = useStyles();
const { t } = useTranslation();
// get ProjectID
const projectID = getProjectID();
const dashboard = useActions(DashboardActions);
const dataSource = useActions(DataSourceActions);
const [cloneName, setCloneName] = React.useState<string>(
`Copy of ${dashboardData.name}`
);
const [isAlertOpen, setIsAlertOpen] = React.useState<boolean>(false);
const onDashboardLoadRoutine = async (dbID: string) => {
dashboard.selectDashboard({
selectedDashboardID: dbID,
refreshRate: 0,
});
dataSource.selectDataSource({
selectedDataSourceURL: '',
selectedDataSourceID: '',
selectedDataSourceName: '',
});
return true;
};
const [createDashboard] = useMutation<CreateDashboardInput>(
CREATE_DASHBOARD,
{
onCompleted: (data) => {
onDashboardLoadRoutine(data.createDashBoard?.db_id ?? '').then(() => {
window.location.reload();
});
},
onError: () => {
setIsAlertOpen(true);
},
}
);
const getPanelGroups = () => {
const panelGroups: PanelGroup[] = [];
dashboardData.metaData[0].panel_groups.forEach((panelGroup) => {
const selectedPanels: Panel[] = [];
panelGroup.panels.forEach((panel) => {
const queries: PromQuery[] = [];
panel.prom_queries.forEach((query) => {
queries.push({
queryid: uuidv4(),
prom_query_name: query.prom_query_name,
legend: query.legend,
resolution: query.resolution,
minstep: query.minstep,
line: query.line,
close_area: query.close_area,
});
});
const options: PanelOption = {
points: panel.panel_options.points,
grids: panel.panel_options.grids,
left_axis: panel.panel_options.left_axis,
};
const selectedPanel: Panel = {
prom_queries: queries,
panel_options: options,
panel_name: panel.panel_name,
y_axis_left: panel.y_axis_left,
y_axis_right: panel.y_axis_right,
x_axis_down: panel.x_axis_down,
unit: panel.unit,
};
selectedPanels.push(selectedPanel);
});
panelGroups.push({
panel_group_name: panelGroup.panel_group_name,
panels: selectedPanels,
});
});
return panelGroups;
};
const getApplicationMetadataMap = () => {
const applicationMetadataMap: ApplicationMetadata[] = [];
dashboardData.applicationMetadataMap?.forEach((applicationMetadata) => {
const applications: Resource[] = [];
applicationMetadata.applications.forEach((application) => {
applications.push({
kind: application.kind,
names: application.names,
});
});
applicationMetadataMap.push({
namespace: applicationMetadata.namespace,
applications,
});
});
return applicationMetadataMap;
};
const handleCreateMutation = () => {
const dashboardInput = {
ds_id: dashboardData.metaData[0].ds_id,
db_name: cloneName,
db_type_id: dashboardData.typeID,
db_type_name: dashboardData.typeName,
db_information: dashboardData.information,
chaos_event_query_template: dashboardData.chaosEventQueryTemplate,
chaos_verdict_query_template: dashboardData.chaosVerdictQueryTemplate,
application_metadata_map: getApplicationMetadataMap(),
panel_groups: getPanelGroups(),
end_time: `${Math.round(new Date().getTime() / 1000)}`,
start_time: `${
Math.round(new Date().getTime() / 1000) - DEFAULT_RELATIVE_TIME_RANGE
}`,
project_id: projectID,
cluster_id: dashboardData.agentID,
refresh_rate: DEFAULT_DASHBOARD_REFRESH_RATE_STRING,
};
createDashboard({
variables: { createDBInput: dashboardInput },
});
};
return (
<div>
<Modal
open
onClose={() => {
onClose();
}}
modalActions={
<ButtonOutlined
className={classes.closeButton}
onClick={() => {
onClose();
}}
>
&#x2715;
</ButtonOutlined>
}
width="45%"
height="fit-content"
>
<div className={classes.modal}>
<Typography className={classes.modalHeading} align="left">
{t(
'analyticsDashboard.monitoringDashboardPage.dashboardCloneModal.heading'
)}
</Typography>
<InputField
label="Name"
data-cy="copyDashboardName"
width="82.5%"
variant={validateTextEmpty(cloneName) ? 'error' : 'primary'}
onChange={(event: React.ChangeEvent<{ value: string }>) => {
setCloneName((event.target as HTMLInputElement).value);
}}
value={cloneName}
/>
<div className={classes.flexButtons}>
<ButtonOutlined
onClick={() => {
onClose();
}}
className={classes.cancelButton}
>
<Typography className={classes.buttonText}>
{t(
'analyticsDashboard.monitoringDashboardPage.dashboardCloneModal.cancel'
)}
</Typography>
</ButtonOutlined>
<ButtonFilled onClick={() => handleCreateMutation()}>
<Typography
className={`${classes.buttonText} ${classes.okButtonText}`}
>
{t(
'analyticsDashboard.monitoringDashboardPage.dashboardCloneModal.ok'
)}
</Typography>
</ButtonFilled>
</div>
</div>
</Modal>
<Snackbar
open={isAlertOpen}
autoHideDuration={6000}
onClose={() => setIsAlertOpen(false)}
>
<Alert onClose={() => setIsAlertOpen(false)} severity="error">
{t(
'analyticsDashboard.monitoringDashboardPage.dashboardCloneModal.error'
)}
</Alert>
</Snackbar>
</div>
);
};
export default DashboardCloneModal;

View File

@ -0,0 +1,44 @@
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles((theme) => ({
modalHeading: {
margin: theme.spacing(6.5, 0, 7.5),
paddingLeft: theme.spacing(6.5),
fontSize: '1.5rem',
},
flexButtons: {
display: 'flex',
justifyContent: 'flex-end',
padding: theme.spacing(5.5, 6.5, 0, 0),
},
modal: {
padding: theme.spacing(5, 0),
},
closeButton: {
borderColor: theme.palette.border.main,
color: theme.palette.border.main,
padding: theme.spacing(0.5),
minWidth: '2.5rem',
},
buttonText: {
lineHeight: '140%',
fontSize: '0.875rem',
},
okButtonText: {
color: theme.palette.text.secondary,
padding: theme.spacing(0, 3),
},
cancelButton: {
borderColor: theme.palette.border.main,
marginRight: theme.spacing(1.5),
padding: theme.spacing(0, 3),
},
}));
export default useStyles;

View File

@ -2,10 +2,6 @@ import { Typography } from '@material-ui/core';
import { ButtonOutlined, Modal } from 'litmus-ui';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
DASHBOARD_TYPE_1,
DASHBOARD_TYPE_2,
} from '../../../../pages/ApplicationDashboard/constants';
import useActions from '../../../../redux/actions';
import * as DashboardActions from '../../../../redux/actions/dashboards';
import { history } from '../../../../redux/configureStore';
@ -17,16 +13,12 @@ import useStyles from './styles';
interface DataSourceInactiveModalProps {
dataSourceStatus: string;
dashboardType: string;
dashboardID: string;
dashboardName: string;
}
const DataSourceInactiveModal: React.FC<DataSourceInactiveModalProps> = ({
dataSourceStatus,
dashboardType,
dashboardID,
dashboardName,
}) => {
const classes = useStyles();
const { t } = useTranslation();
@ -66,16 +58,9 @@ const DataSourceInactiveModal: React.FC<DataSourceInactiveModalProps> = ({
<div className={classes.flexButtons}>
<ButtonOutlined
onClick={() => {
let dashboardTemplateID: number = -1;
if (dashboardType === DASHBOARD_TYPE_1) {
dashboardTemplateID = 0;
} else if (dashboardType === DASHBOARD_TYPE_2) {
dashboardTemplateID = 1;
}
dashboard.selectDashboard({
selectedDashboardID: dashboardID,
selectedDashboardName: dashboardName,
selectedDashboardTemplateID: dashboardTemplateID,
activePanelID: '',
});
history.push({
pathname: '/analytics/dashboard/configure',

View File

@ -3,7 +3,6 @@ import Icon from '@material-ui/core/Icon';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { CheckBox } from '../../../../components/CheckBox';
import DashboardList from '../../../../components/PreconfiguredDashboards/data';
import {
DashboardConfigurationDetails,
PanelNameAndID,
@ -84,16 +83,12 @@ const InfoDropdown: React.FC<InfoDropdownProps> = ({
</Typography>
<div className={classes.iconWithTextDiv}>
<img
src={`/icons/${
dashboardConfigurationDetails.type === DashboardList[0].name
? 'kubernetes-platform'
: 'sock-shop'
}.svg`}
src={`/icons/${dashboardConfigurationDetails.typeID}_dashboard.svg`}
alt="dashboard Icon"
className={classes.inlineIcon}
/>
<Typography className={classes.infoValue}>
{dashboardConfigurationDetails.type}
{dashboardConfigurationDetails.typeID}
</Typography>
</div>
</div>
@ -132,7 +127,7 @@ const InfoDropdown: React.FC<InfoDropdownProps> = ({
)}
</Typography>
<Typography className={classes.infoValue}>
{dashboardConfigurationDetails.agent}
{dashboardConfigurationDetails.agentName}
</Typography>
</div>
</div>
@ -143,8 +138,8 @@ const InfoDropdown: React.FC<InfoDropdownProps> = ({
)}
</Typography>
<div className={classes.checkBoxesContainer}>
{applicationsToBeShown.map((application: string) => (
<FormGroup key="application-group">
<FormGroup key="application-group">
{applicationsToBeShown.map((application: string) => (
<FormControlLabel
control={
<CheckBox
@ -153,10 +148,15 @@ const InfoDropdown: React.FC<InfoDropdownProps> = ({
name={application}
/>
}
label={application}
label={
<Typography className={classes.formControlLabel}>
{application}
</Typography>
}
key={`${application}-application-label`}
/>
</FormGroup>
))}
))}
</FormGroup>
</div>
</div>
<div className={classes.infoSectionElement}>
@ -166,8 +166,8 @@ const InfoDropdown: React.FC<InfoDropdownProps> = ({
)}
</Typography>
<div className={classes.checkBoxesContainer}>
{metricsToBeShown.map((metric: PanelNameAndID) => (
<FormGroup key="application-group">
<FormGroup key="metric-group">
{metricsToBeShown.map((metric: PanelNameAndID) => (
<FormControlLabel
control={
<CheckBox
@ -176,11 +176,15 @@ const InfoDropdown: React.FC<InfoDropdownProps> = ({
name={metric.name}
/>
}
label={metric.name}
className={classes.formControlLabel}
label={
<Typography className={classes.formControlLabel}>
{metric.name}
</Typography>
}
key={`${metric}-metric-label`}
/>
</FormGroup>
))}
))}
</FormGroup>
</div>
</div>
</div>

View File

@ -3,6 +3,7 @@ import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles((theme) => ({
root: {
width: '100%',
marginTop: theme.spacing(3.5),
},
header: {
background: theme.palette.cards.header,
@ -26,10 +27,11 @@ const useStyles = makeStyles((theme) => ({
padding: theme.spacing(2, 2, 3, 2),
},
sectionHeader: {
lineHeight: '130%',
lineHeight: '150%',
fontSize: '1rem',
fontWeight: 500,
padding: theme.spacing(1, 0, 3),
letterSpacing: '0.1714px',
},
dashboardMetaDataItem: {
display: 'grid',
@ -37,26 +39,23 @@ const useStyles = makeStyles((theme) => ({
marginBottom: theme.spacing(0.75),
},
infoKey: {
fontWeight: 500,
fontSize: '0.875rem',
letterSpacing: '0.02em',
lineHeight: '150%',
fontSize: '0.825rem',
lineHeight: '0.9375rem',
color: theme.palette.highlight,
},
infoValue: {
fontSize: '0.875rem',
letterSpacing: '0.02em',
fontSize: '0.825rem',
lineHeight: '150%',
},
checkBoxesContainer: {
display: 'grid',
gridTemplateColumns: '1fr 1fr',
maxHeight: '8rem',
maxHeight: '7rem',
overflowY: 'scroll',
paddingLeft: theme.spacing(1),
},
formControlLabel: {
fontSize: '0.625rem',
fontSize: '0.75rem',
letterSpacing: '0.02em',
lineHeight: '150%',
},

View File

@ -28,11 +28,16 @@ import {
} from '../../../../pages/ApplicationDashboard/constants';
import useActions from '../../../../redux/actions';
import * as DashboardActions from '../../../../redux/actions/dashboards';
import { history } from '../../../../redux/configureStore';
import { RootState } from '../../../../redux/reducers';
import { ReactComponent as ViewChaosMetric } from '../../../../svg/aligment.svg';
import { ReactComponent as DisableViewChaosMetric } from '../../../../svg/alignmentStriked.svg';
import { ReactComponent as Expand } from '../../../../svg/arrowsOut.svg';
import { ReactComponent as Edit } from '../../../../svg/edit.svg';
import {
getProjectID,
getProjectRole,
} from '../../../../utils/getSearchParams';
import {
DataParserForPrometheus,
getPromQueryInput,
@ -56,10 +61,13 @@ const PanelContent: React.FC<GraphPanelProps> = ({
const { palette } = useTheme();
const classes = useStyles();
const { t } = useTranslation();
// get ProjectID
const projectID = getProjectID();
const projectRole = getProjectRole();
const dashboard = useActions(DashboardActions);
const lineGraph: string[] = palette.graph.line;
const areaGraph: string[] = palette.graph.area;
const [popout, setPopout] = useState(false);
const [popOut, setPopOut] = useState(false);
const [viewEventMetric, setViewEventMetric] = useState(false);
const [prometheusQueryData, setPrometheusQueryData] =
React.useState<PrometheusQueryDataInterface>({
@ -76,6 +84,7 @@ const PanelContent: React.FC<GraphPanelProps> = ({
const [graphData, setGraphData] = React.useState<ParsedPrometheusData>({
seriesData: [],
closedAreaData: [],
chaosData: [],
});
@ -108,10 +117,16 @@ const PanelContent: React.FC<GraphPanelProps> = ({
const parsedData: ParsedPrometheusData = DataParserForPrometheus(
prometheusData,
lineGraph,
areaGraph
areaGraph,
prom_queries
.filter((query) => query.close_area)
.map((query) => query.queryid)
);
setGraphData(parsedData);
}
dashboard.selectDashboard({
forceUpdate: false,
});
},
onError: (error: ApolloError) => {
if (error.message === PROMETHEUS_ERROR_QUERY_RESOLUTION_LIMIT_REACHED) {
@ -296,9 +311,17 @@ const PanelContent: React.FC<GraphPanelProps> = ({
</ToolTip>
)}
<IconButton
disabled
className={classes.panelIconButton}
onClick={() => {}}
onClick={() => {
dashboard.selectDashboard({
selectedDashboardID: selectedDashboard.selectedDashboardID,
activePanelID: panel_id,
});
history.push({
pathname: '/analytics/dashboard/configure',
search: `?projectID=${projectID}&projectRole=${projectRole}`,
});
}}
>
<Edit className={classes.panelIcon} />
</IconButton>
@ -306,7 +329,7 @@ const PanelContent: React.FC<GraphPanelProps> = ({
<IconButton
className={classes.panelIconButton}
onClick={() => {
setPopout(true);
setPopOut(true);
}}
>
<Expand className={classes.panelIcon} />
@ -316,12 +339,12 @@ const PanelContent: React.FC<GraphPanelProps> = ({
</div>
<div>
<Modal
open={popout}
onClose={() => setPopout(false)}
open={popOut}
onClose={() => setPopOut(false)}
disableBackdropClick
disableEscapeKeyDown
modalActions={
<ButtonOutlined onClick={() => setPopout(false)}>
<ButtonOutlined onClick={() => setPopOut(false)}>
&#x2715;
</ButtonOutlined>
}
@ -333,11 +356,12 @@ const PanelContent: React.FC<GraphPanelProps> = ({
<LineAreaGraph
legendTableHeight={120}
openSeries={graphData.seriesData}
closedSeries={graphData.closedAreaData}
eventSeries={graphData.chaosData}
showPoints={false}
showLegendTable
showEventTable
showTips
showTips={false}
showEventMarkers
marginLeftEventTable={10}
unit={unit}
@ -352,11 +376,12 @@ const PanelContent: React.FC<GraphPanelProps> = ({
<LineAreaGraph
legendTableHeight={120}
openSeries={graphData.seriesData}
closedSeries={graphData.closedAreaData}
eventSeries={graphData.chaosData}
showPoints={false}
showEventTable={viewEventMetric}
showLegendTable
showTips
showTips={false}
showEventMarkers
unit={unit}
yLabel={y_axis_left}

View File

@ -54,6 +54,7 @@ const DashboardPanelGroupContent: React.FC<DashboardPanelGroupContentProps> = ({
key={panel.panel_id}
data-cy="dashboardPanel"
panel_id={panel.panel_id}
created_at={panel.created_at}
panel_name={panel.panel_name}
panel_options={panel.panel_options}
prom_queries={panel.prom_queries}

View File

@ -15,13 +15,15 @@ const useStyles = makeStyles((theme) => ({
display: 'inline-block',
background: theme.palette.background.paper,
padding: theme.spacing(2, 2, 0, 2),
marginBottom: theme.spacing(1),
},
panelGroup: {
display: 'flex',
alignContent: 'left',
background: theme.palette.cards.header,
boxShadow:
'0px 0.3px 0.9px rgba(0, 0, 0, 0.1), 0px 1.6px 3.6px rgba(0, 0, 0, 0.13)',
borderRadius: '3px 3px 0px 0px',
},
panelGroupContainer: {
@ -32,6 +34,8 @@ const useStyles = makeStyles((theme) => ({
gridTemplateColumns: '49% 49%',
gridGap: theme.spacing(1.75),
padding: theme.spacing(1, 1, 1, 1.75),
boxShadow:
'0px 0.3px 0.9px rgba(0, 0, 0, 0.1), 0px 1.6px 3.6px rgba(0, 0, 0, 0.13)',
},
expand: {

View File

@ -0,0 +1,310 @@
import {
FormControl,
IconButton,
MenuItem,
OutlinedInput,
Select,
Typography,
} from '@material-ui/core';
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
import WatchLaterRoundedIcon from '@material-ui/icons/WatchLaterRounded';
import { ButtonOutlined } from 'litmus-ui';
import moment from 'moment';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import DateRangeSelector from '../../../../components/DateRangeSelector';
import {
DEFAULT_REFRESH_RATE,
DEFAULT_TOLERANCE_LIMIT,
MAX_REFRESH_RATE,
MINIMUM_TOLERANCE_LIMIT,
} from '../../../../pages/ApplicationDashboard/constants';
import refreshData from '../../../../pages/ApplicationDashboard/refreshData';
import useActions from '../../../../redux/actions';
import * as DashboardActions from '../../../../redux/actions/dashboards';
import { RootState } from '../../../../redux/reducers';
import useStyles, { useOutlinedInputStyles } from './styles';
interface RefreshObjectType {
label: string;
value: number;
}
const ToolBar: React.FC = () => {
const classes = useStyles();
const outlinedInputClasses = useOutlinedInputStyles();
const { t } = useTranslation();
const dashboard = useActions(DashboardActions);
const selectedDashboard = useSelector(
(state: RootState) => state.selectDashboard
);
const dateRangeSelectorRef = React.useRef<HTMLDivElement>(null);
const [isDateRangeSelectorPopoverOpen, setDateRangeSelectorPopoverOpen] =
React.useState(false);
const [refreshRate, setRefreshRate] = React.useState<number>(
selectedDashboard.refreshRate ? selectedDashboard.refreshRate : 0
);
const [openRefresh, setOpenRefresh] = React.useState(false);
const handleCloseRefresh = () => {
setOpenRefresh(false);
};
const handleOpenRefresh = () => {
setOpenRefresh(true);
};
const clearTimeOuts = async () => {
let id = window.setTimeout(() => {}, 0);
while (id--) {
window.clearTimeout(id);
}
return Promise.resolve(id === 0);
};
const CallbackFromRangeSelector = (
selectedStartDate: string,
selectedEndDate: string
) => {
const startDateFormatted: string = moment(selectedStartDate).format();
const endDateFormatted: string = moment(selectedEndDate)
.add(23, 'hours')
.add(59, 'minutes')
.add(59, 'seconds')
.format();
dashboard.selectDashboard({
range: { startDate: startDateFormatted, endDate: endDateFormatted },
});
const endDate: number =
new Date(moment(endDateFormatted).format()).getTime() / 1000;
const now: number = Math.round(new Date().getTime() / 1000);
const diff: number = Math.abs(now - endDate);
const maxLim: number =
(selectedDashboard.refreshRate ?? DEFAULT_REFRESH_RATE) / 1000 !== 0
? (selectedDashboard.refreshRate ?? DEFAULT_REFRESH_RATE) / 1000 +
MINIMUM_TOLERANCE_LIMIT
: DEFAULT_TOLERANCE_LIMIT;
if (
!(diff >= 0 && diff <= maxLim) &&
selectedDashboard.refreshRate !== MAX_REFRESH_RATE
) {
clearTimeOuts().then(() => {
setRefreshRate(MAX_REFRESH_RATE);
});
}
};
const getRefreshRateStatus = () => {
if (selectedDashboard.range) {
const endDate: number =
new Date(moment(selectedDashboard.range.endDate).format()).getTime() /
1000;
const now: number = Math.round(new Date().getTime() / 1000);
const diff: number = Math.abs(now - endDate);
const maxLim: number =
(selectedDashboard.refreshRate ?? DEFAULT_REFRESH_RATE) / 1000 !== 0
? (selectedDashboard.refreshRate ?? DEFAULT_REFRESH_RATE) / 1000 +
MINIMUM_TOLERANCE_LIMIT
: DEFAULT_TOLERANCE_LIMIT;
if (!(diff >= 0 && diff <= maxLim)) {
// A non relative time range has been selected.
// Refresh rate switch is not acknowledged and it's state is locked (Off).
// Select a relative time range or select a different refresh rate to unlock again.
return true;
}
}
// For relative time ranges.
return false;
};
return (
<div className={classes.headerDiv}>
<Typography className={classes.headerInfoText}>
{t('analyticsDashboard.monitoringDashboardPage.headerInfoText')}
</Typography>
<div className={classes.controls}>
<div ref={dateRangeSelectorRef}>
<ButtonOutlined
className={classes.selectDate}
onClick={() => setDateRangeSelectorPopoverOpen(true)}
aria-label="time range"
aria-haspopup="true"
>
<Typography className={classes.displayDate}>
<IconButton className={classes.rangeSelectorClockIcon}>
<WatchLaterRoundedIcon />
</IconButton>
{!selectedDashboard.range ||
selectedDashboard.range.startDate === ' '
? `${t(
'analyticsDashboard.monitoringDashboardPage.rangeSelector.selectPeriod'
)}`
: `${selectedDashboard.range.startDate.split('-')[0]}-${
selectedDashboard.range.startDate.split('-')[1]
}-${selectedDashboard.range.startDate.substring(
selectedDashboard.range.startDate.lastIndexOf('-') + 1,
selectedDashboard.range.startDate.lastIndexOf('T')
)}
${selectedDashboard.range.startDate.substring(
selectedDashboard.range.startDate.lastIndexOf('T') + 1,
selectedDashboard.range.startDate.lastIndexOf('+')
)}
${t(
'analyticsDashboard.monitoringDashboardPage.rangeSelector.to'
)}
${selectedDashboard.range.endDate.split('-')[0]}-${
selectedDashboard.range.endDate.split('-')[1]
}-${selectedDashboard.range.endDate.substring(
selectedDashboard.range.endDate.lastIndexOf('-') + 1,
selectedDashboard.range.endDate.lastIndexOf('T')
)}
${selectedDashboard.range.endDate.substring(
selectedDashboard.range.endDate.lastIndexOf('T') + 1,
selectedDashboard.range.endDate.lastIndexOf('+')
)}`}
<IconButton className={classes.rangeSelectorIcon}>
<KeyboardArrowDownIcon />
</IconButton>
</Typography>
</ButtonOutlined>
</div>
<DateRangeSelector
anchorEl={dateRangeSelectorRef.current}
isOpen={isDateRangeSelectorPopoverOpen}
onClose={() => {
setDateRangeSelectorPopoverOpen(false);
}}
callbackToSetRange={CallbackFromRangeSelector}
className={classes.rangeSelectorPopover}
/>
<div className={classes.refreshDiv}>
<ButtonOutlined
onClick={() => {
clearTimeOuts().then(() => {
dashboard.selectDashboard({
forceUpdate: true,
});
});
}}
className={classes.refreshButton}
>
<img
src="/icons/refresh-dashboard.svg"
alt="refresh icon"
className={classes.refreshIcon}
/>
</ButtonOutlined>
<FormControl className={classes.formControl} variant="outlined">
<Select
labelId="refresh-controlled-open-select-label"
id="refresh-controlled-open-select"
open={openRefresh}
disabled={getRefreshRateStatus()}
onClose={handleCloseRefresh}
onOpen={handleOpenRefresh}
native={false}
displayEmpty
value={refreshRate !== 0 ? refreshRate : null}
onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
// When viewing data for non-relative time range, refresh should be Off ideally.
// UI can auto detect if it is not Off and switches it to Off.
// Now the user can try to view the non-relative time range data again.
if (selectedDashboard.refreshRate !== MAX_REFRESH_RATE) {
dashboard.selectDashboard({
refreshRate: event.target.value as number,
});
setRefreshRate(event.target.value as number);
} else {
dashboard.selectDashboard({
refreshRate: event.target.value as number,
});
setRefreshRate(event.target.value as number);
dashboard.selectDashboard({
forceUpdate: true,
});
}
}}
input={<OutlinedInput classes={outlinedInputClasses} />}
renderValue={() => (
<div>
{t(
'analyticsDashboard.monitoringDashboardPage.refresh.heading'
)}
</div>
)}
IconComponent={KeyboardArrowDownIcon}
MenuProps={{
anchorOrigin: {
vertical: 'bottom',
horizontal: 'right',
},
transformOrigin: {
vertical: 'top',
horizontal: 'right',
},
getContentAnchorEl: null,
}}
>
<MenuItem
key="Off-refresh-option"
value={MAX_REFRESH_RATE}
className={classes.menuListItem}
>
<Typography
className={classes.menuListItemText}
style={{
fontWeight: refreshRate === MAX_REFRESH_RATE ? 500 : 400,
}}
>
{t('analyticsDashboard.monitoringDashboardPage.refresh.off')}
</Typography>
{refreshRate === MAX_REFRESH_RATE ? (
<img
src="/icons/check.svg"
alt="check mark"
className={classes.checkIcon}
/>
) : (
<></>
)}
</MenuItem>
{refreshData.map((data: RefreshObjectType) => (
<MenuItem
key={`${data.label}-refresh-option`}
value={data.value}
className={classes.menuListItem}
>
<Typography
className={classes.menuListItemText}
style={{
fontWeight: refreshRate === data.value ? 500 : 400,
}}
>
{t(data.label)}
</Typography>
{refreshRate === data.value ? (
<img
src="/icons/check.svg"
alt="check mark"
className={classes.checkIcon}
/>
) : (
<></>
)}
</MenuItem>
))}
</Select>
</FormControl>
</div>
</div>
</div>
);
};
export default ToolBar;

View File

@ -0,0 +1,151 @@
import { makeStyles, Theme } from '@material-ui/core';
const useStyles = makeStyles((theme) => ({
headerDiv: {
display: 'flex',
justifyContent: 'space-between',
marginTop: theme.spacing(3.5),
paddingTop: theme.spacing(2.15),
backgroundColor: theme.palette.cards.header,
minHeight: '5rem',
},
selectDate: {
display: 'flex',
height: '2.9rem',
minWidth: '9rem',
marginLeft: theme.spacing(3.75),
border: `0.1px solid ${theme.palette.border.main}`,
background: theme.palette.background.paper,
borderRadius: '0.25rem',
textTransform: 'none',
'&:hover': {
borderColor: theme.palette.primary.main,
background: theme.palette.background.paper,
},
'&$focused': {
borderColor: theme.palette.primary.main,
background: theme.palette.background.paper,
},
},
displayDate: {
width: '100%',
color: theme.palette.text.primary,
},
rangeSelectorIcon: {
width: '0.625rem',
height: '0.625rem',
margin: theme.spacing(0, -1, 0, 1),
},
rangeSelectorClockIcon: {
width: '0.825rem',
height: '0.825rem',
margin: theme.spacing(0, 1, 0, -1),
},
rangeSelectorPopover: {
margin: theme.spacing(-2, 0, 0, -4.25),
},
menuListItem: {
background: `${theme.palette.background.paper} !important`,
fontSize: '0.875rem',
lineHeight: '150%',
height: '1.75rem',
width: '11.5rem',
display: 'flex',
justifyContent: 'space-between',
},
menuListItemText: {
fontSize: '0.875rem',
},
checkIcon: {
margin: theme.spacing(0, 1),
},
refreshDiv: {
display: 'flex',
},
refreshButton: {
padding: theme.spacing(1.6),
minWidth: '2rem',
background: theme.palette.background.paper,
borderColor: theme.palette.border.main,
'&:hover': {
borderColor: theme.palette.primary.main,
background: theme.palette.background.paper,
},
marginLeft: theme.spacing(1.5),
[theme.breakpoints.down('sm')]: {
width: 'fit-content',
marginLeft: theme.spacing(3.75),
},
},
refreshIcon: {
width: '1.15rem',
height: '1.15rem',
},
headerInfoText: {
fontWeight: 500,
fontSize: '0.9rem',
lineHeight: '150%',
letterSpacing: '0.02em',
width: '35%',
marginLeft: theme.spacing(2.5),
},
controls: {
marginRight: theme.spacing(2.25),
display: 'flex',
[theme.breakpoints.down('sm')]: {
flexDirection: 'column',
marginRight: 0,
},
},
formControl: {
width: '6rem',
'& .MuiSelect-outlined': {
padding: '0.925rem',
'&:focus': {
borderColor: theme.palette.primary.main,
},
'& .MuiInputLabel-root': {
color: `${theme.palette.text.hint} !important`,
marginTop: `${theme.spacing(2)} !important`,
},
},
},
}));
export const useOutlinedInputStyles = makeStyles((theme: Theme) => ({
root: {
'& $notchedOutline': {
borderColor: theme.palette.border.main,
},
'&:hover $notchedOutline': {
borderColor: theme.palette.primary.main,
},
'&$focused $notchedOutline': {
borderColor: theme.palette.primary.main,
},
'& .MuiInputLabel-root': {
color: `${theme.palette.text.hint} !important`,
marginTop: `${theme.spacing(2)} !important`,
},
color: theme.palette.text.hint,
background: theme.palette.background.paper,
},
focused: {},
notchedOutline: {},
}));
export default useStyles;

View File

@ -1,35 +1,166 @@
import { Typography } from '@material-ui/core';
/* eslint-disable no-unused-expressions */
import { MenuItem, Typography } from '@material-ui/core';
import { ButtonFilled, ButtonOutlined } from 'litmus-ui';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { StyledMenu } from '../../../../components/StyledMenu';
import {
DashboardExport,
PanelExport,
PanelGroupExport,
PanelGroupMap,
PromQueryExport,
SelectedDashboardInformation,
} from '../../../../models/dashboardsData';
import {
ApplicationMetadata,
PanelOption,
Resource,
} from '../../../../models/graphql/dashboardsDetails';
import useActions from '../../../../redux/actions';
import * as DashboardActions from '../../../../redux/actions/dashboards';
import { history } from '../../../../redux/configureStore';
import {
getProjectID,
getProjectRole,
} from '../../../../utils/getSearchParams';
import DashboardCloneModal from '../DashboardCloneModal';
import useStyles from './styles';
interface TopNavButtonsProps {
isInfoToggledState: Boolean;
switchIsInfoToggled: (toggleState: Boolean) => void;
dashboardData: SelectedDashboardInformation;
dashboardTypeID: string;
}
interface NavButtonStates {
isInfoToggled: Boolean;
isOptionsToggled: Boolean;
}
const TopNavButtons: React.FC<TopNavButtonsProps> = ({
isInfoToggledState,
switchIsInfoToggled,
dashboardData,
dashboardTypeID,
}) => {
const classes = useStyles();
const { t } = useTranslation();
const projectID = getProjectID();
const projectRole = getProjectRole();
const dashboard = useActions(DashboardActions);
const [isInfoToggled, setIsInfoToggled] =
React.useState<Boolean>(isInfoToggledState);
const [navButtonStates, setNavButtonStates] = React.useState<NavButtonStates>(
{
isInfoToggled: isInfoToggledState,
isOptionsToggled: false,
}
);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const anchorRef = React.useRef<HTMLDivElement>(null);
const [cloneModalOpen, setCloneModalOpen] = React.useState<Boolean>(false);
const getDashboard = () => {
const panelGroupMap: PanelGroupMap[] = [];
const panelGroups: PanelGroupExport[] = [];
dashboardData.metaData[0].panel_groups.forEach((panelGroup) => {
panelGroupMap.push({
groupName: panelGroup.panel_group_name,
panels: [],
});
const len: number = panelGroupMap.length;
const selectedPanels: PanelExport[] = [];
panelGroup.panels.forEach((panel) => {
panelGroupMap[len - 1].panels.push(panel.panel_name);
const queries: PromQueryExport[] = [];
panel.prom_queries.forEach((query) => {
queries.push({
prom_query_name: query.prom_query_name,
legend: query.legend,
resolution: query.resolution,
minstep: query.minstep,
line: query.line,
close_area: query.close_area,
});
});
const options: PanelOption = {
points: panel.panel_options.points,
grids: panel.panel_options.grids,
left_axis: panel.panel_options.left_axis,
};
const selectedPanel: PanelExport = {
prom_queries: queries,
panel_options: options,
panel_name: panel.panel_name,
y_axis_left: panel.y_axis_left,
y_axis_right: panel.y_axis_right,
x_axis_down: panel.x_axis_down,
unit: panel.unit,
};
selectedPanels.push(selectedPanel);
});
panelGroups.push({
panel_group_name: panelGroup.panel_group_name,
panels: selectedPanels,
});
});
const applicationMetadataMap: ApplicationMetadata[] = [];
dashboardData.applicationMetadataMap?.forEach((applicationMetadata) => {
const applications: Resource[] = [];
applicationMetadata.applications.forEach((application) => {
applications.push({
kind: application.kind,
names: application.names,
});
});
applicationMetadataMap.push({
namespace: applicationMetadata.namespace,
applications,
});
});
const exportedDashboard: DashboardExport = {
dashboardID: dashboardTypeID,
name: dashboardData.name,
information: dashboardData.information,
chaosEventQueryTemplate: dashboardData.chaosEventQueryTemplate,
chaosVerdictQueryTemplate: dashboardData.chaosVerdictQueryTemplate,
applicationMetadataMap,
panelGroupMap,
panelGroups,
};
return exportedDashboard;
};
// Function to download the JSON
const downloadJSON = () => {
const element = document.createElement('a');
const file = new Blob([JSON.stringify(getDashboard(), null, 2)], {
type: 'text/json',
});
element.href = URL.createObjectURL(file);
element.download = `${dashboardData.name}.json`;
document.body.appendChild(element);
element.click();
};
return (
<div className={classes.button}>
{isInfoToggled ? (
{navButtonStates.isInfoToggled ? (
<ButtonFilled
onClick={() => {
setIsInfoToggled(false);
setNavButtonStates({ ...navButtonStates, isInfoToggled: false });
switchIsInfoToggled(false);
}}
>
<img
src="/icons/infoWhite.svg"
alt="Info Icon"
alt="Info icon"
className={classes.icon}
/>
<Typography className={classes.infoText}>
@ -39,16 +170,176 @@ const TopNavButtons: React.FC<TopNavButtonsProps> = ({
) : (
<ButtonOutlined
onClick={() => {
setIsInfoToggled(true);
setNavButtonStates({ ...navButtonStates, isInfoToggled: true });
switchIsInfoToggled(true);
}}
>
<img src="/icons/info.svg" alt="Info Icon" className={classes.icon} />
<img src="/icons/info.svg" alt="Info icon" className={classes.icon} />
<Typography className={classes.infoText}>
{t('analyticsDashboard.monitoringDashboardPage.infoButtonText')}
</Typography>
</ButtonOutlined>
)}
<div ref={anchorRef}>
{navButtonStates.isOptionsToggled ? (
<ButtonFilled
onClick={() => {
setAnchorEl(null);
setNavButtonStates({
...navButtonStates,
isOptionsToggled: false,
});
}}
className={classes.optionsButton}
>
<img
src="/icons/menu-active.svg"
alt="Options icon"
className={classes.menuIcon}
/>
</ButtonFilled>
) : (
<ButtonOutlined
onClick={(event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
setNavButtonStates({
...navButtonStates,
isOptionsToggled: true,
});
}}
className={classes.optionsButton}
>
<img
src="/icons/menu.svg"
alt="Options icon"
className={classes.menuIcon}
/>
</ButtonOutlined>
)}
</div>
<StyledMenu
id="long-menu"
anchorEl={anchorRef.current}
elevation={0}
getContentAnchorEl={null}
keepMounted
open={Boolean(anchorEl)}
onClose={() => {
setAnchorEl(null);
setNavButtonStates({ ...navButtonStates, isOptionsToggled: false });
}}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
className={classes.menuList}
>
<MenuItem
value="Configure"
onClick={() => {
dashboard.selectDashboard({
selectedDashboardID: dashboardData.id,
activePanelID: '',
});
history.push({
pathname: '/analytics/dashboard/configure',
search: `?projectID=${projectID}&projectRole=${projectRole}`,
});
}}
className={classes.menuItem}
>
<div className={classes.expDiv}>
<img
src="/icons/cogWheel.svg"
alt="Configure"
className={classes.btnImg}
/>
<Typography
data-cy="optionsConfigureDashboard"
className={classes.btnText}
>
{t('analyticsDashboardViews.kubernetesDashboard.table.configure')}
</Typography>
</div>
</MenuItem>
<MenuItem
value="Make a copy"
onClick={() => {
setCloneModalOpen(true);
}}
className={classes.menuItem}
>
<div className={classes.expDiv}>
<img
src="/icons/copy-dashboard.svg"
alt="Make a copy"
className={classes.btnImg}
/>
<Typography
data-cy="optionsCopyDashboard"
className={classes.btnText}
>
{t('analyticsDashboard.monitoringDashboardPage.options.clone')}
</Typography>
</div>
</MenuItem>
<MenuItem
value="Download json"
onClick={() => downloadJSON()}
className={classes.menuItem}
>
<div className={classes.expDiv}>
<img
src="/icons/download-dashboard.svg"
alt="Download json"
className={classes.btnImg}
/>
<Typography
data-cy="optionsDownloadDashboard"
className={classes.btnText}
>
{t('analyticsDashboard.monitoringDashboardPage.options.json')}
</Typography>
</div>
</MenuItem>
<MenuItem
value="Export pdf"
onClick={() => {}}
className={classes.menuItem}
disabled
>
<div className={classes.expDiv}>
<img
src="/icons/export-dashboard.svg"
alt="Export pdf"
className={classes.btnImg}
/>
<Typography
data-cy="optionsExportDashboard"
className={classes.btnText}
>
{t('analyticsDashboard.monitoringDashboardPage.options.pdf')}
</Typography>
</div>
</MenuItem>
</StyledMenu>
{cloneModalOpen ? (
<DashboardCloneModal
dashboardData={dashboardData}
onClose={() => {
setCloneModalOpen(false);
}}
/>
) : (
<div />
)}
</div>
);
};

View File

@ -13,6 +13,37 @@ const useStyles = makeStyles((theme) => ({
infoText: {
paddingRight: theme.spacing(1.5),
},
menuIcon: {
margin: theme.spacing(0, 1),
width: '1.25rem',
},
optionsButton: {
marginLeft: theme.spacing(2),
minWidth: 0,
},
// Menu option with icon
menuList: {
marginLeft: theme.spacing(-1),
},
menuItem: {
width: '11.5rem',
height: '2.5rem',
},
expDiv: {
display: 'flex',
flexDirection: 'row',
},
btnImg: {
width: '1rem',
height: '1rem',
marginTop: theme.spacing(0.25),
},
btnText: {
fontSize: '0.875rem',
lineHeight: '140%',
paddingLeft: theme.spacing(1.625),
},
}));
export default useStyles;

View File

@ -1,39 +0,0 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/no-static-element-interactions */
import React from 'react';
import { DashboardData } from '../../../../models/dashboardsData';
import useStyles from '../../DataSources/Cards/styles';
const CardContent: React.FC<DashboardData> = ({
name,
urlToIcon,
handleClick,
description,
}) => {
const classes = useStyles();
return (
<div className={classes.card}>
<div className={classes.cardContent} onClick={handleClick}>
<div>
{urlToIcon ? (
<div className={classes.cardMedia}>
<img src={urlToIcon} alt="chart provider logo" />
</div>
) : (
<div className={classes.noImage}>Image</div>
)}
<div data-cy="dashboardName" className={classes.title}>
{name}
</div>
{description ? (
<div className={classes.description}>{description}</div>
) : (
<span />
)}
</div>
</div>
</div>
);
};
export default CardContent;

View File

@ -1,51 +0,0 @@
import React from 'react';
import { DashboardData } from '../../../../models/dashboardsData';
import useActions from '../../../../redux/actions';
import * as DashboardActions from '../../../../redux/actions/dashboards';
import { history } from '../../../../redux/configureStore';
import {
getProjectID,
getProjectRole,
} from '../../../../utils/getSearchParams';
import DashboardCard from './index';
import useStyles from '../../DataSources/Cards/styles';
interface DashboardCardsProps {
dashboards: DashboardData[];
}
const DashboardCards: React.FC<DashboardCardsProps> = ({ dashboards }) => {
const classes = useStyles();
const projectID = getProjectID();
const projectRole = getProjectRole();
const dashboard = useActions(DashboardActions);
return (
<div className={classes.root}>
{dashboards &&
dashboards.map((d: DashboardData) => (
<div key={d.dashboardID} data-cy="dashboardCard">
<DashboardCard
key={d.dashboardID}
name={d.name}
urlToIcon={d.urlToIcon}
handleClick={() => {
dashboard.selectDashboard({
selectedDashboardTemplateID: d.dashboardID,
selectedDashboardTemplateName: d.name,
selectedDashboardDescription: d.information,
selectedDashboardPanelGroupMap: d.panelGroupMap,
});
history.push({
pathname: '/analytics/dashboard/create',
search: `?projectID=${projectID}&projectRole=${projectRole}`,
});
}}
description={d.description}
/>
</div>
))}
</div>
);
};
export default DashboardCards;

View File

@ -1,395 +0,0 @@
/* eslint-disable no-unused-expressions */
import { useQuery } from '@apollo/client';
import {
FormControl,
InputLabel,
MenuItem,
Select,
Typography,
} from '@material-ui/core';
import Divider from '@material-ui/core/Divider';
import { InputField } from 'litmus-ui';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import PreConfiguredDashboards from '../../../../components/PreconfiguredDashboards/data';
import {
GET_CLUSTER,
LIST_DASHBOARD,
LIST_DATASOURCE,
} from '../../../../graphql';
import {
DashboardDetails,
PanelGroupMap,
} from '../../../../models/dashboardsData';
import {
Cluster,
Clusters,
ClusterVars,
} from '../../../../models/graphql/clusterData';
import {
DashboardList,
ListDashboardResponse,
ListDashboardVars,
} from '../../../../models/graphql/dashboardsDetails';
import {
DataSourceList,
ListDataSourceResponse,
ListDataSourceVars,
} from '../../../../models/graphql/dataSourceDetails';
import { RootState } from '../../../../redux/reducers';
import { getProjectID } from '../../../../utils/getSearchParams';
import { validateTextEmpty } from '../../../../utils/validate';
import useStyles from './styles';
interface ConfigureDashboardProps {
configure: boolean;
dashboardID?: string;
CallbackToSetVars: (vars: DashboardDetails) => void;
}
const ConfigureDashboard: React.FC<ConfigureDashboardProps> = ({
configure,
dashboardID,
CallbackToSetVars,
}) => {
const classes = useStyles();
const { t } = useTranslation();
const projectID = getProjectID();
const selectedDashboard = useSelector(
(state: RootState) => state.selectDashboard
);
const selectedDashboardID = useSelector(
(state: RootState) => state.selectDashboard.selectedDashboardID
);
const [dashboardDetails, setDashboardDetails] = useState<DashboardDetails>({
id: selectedDashboardID,
name: '',
dashboardType: selectedDashboard.selectedDashboardTemplateName ?? '',
dataSourceType: '',
dataSourceID: '',
agentID: '',
dataSourceName: '',
agentName: '',
information: selectedDashboard.selectedDashboardDescription ?? '',
panelGroupMap: selectedDashboard.selectedDashboardPanelGroupMap ?? [],
panelGroups: [],
});
const [update, setUpdate] = useState(false);
// Apollo query to get the datasource data
const { data: dataSourceList } = useQuery<DataSourceList, ListDataSourceVars>(
LIST_DATASOURCE,
{
variables: { projectID },
fetchPolicy: 'cache-and-network',
}
);
// Apollo query to get the agent data
const { data: agentList } = useQuery<Clusters, ClusterVars>(GET_CLUSTER, {
variables: { project_id: projectID },
fetchPolicy: 'cache-and-network',
});
// Apollo query to get the dashboard data
const { data: dashboardList } = useQuery<DashboardList, ListDashboardVars>(
LIST_DASHBOARD,
{
variables: { projectID },
fetchPolicy: 'cache-and-network',
}
);
const getDataSourceType = (searchingData: ListDataSourceResponse[]) => {
const uniqueList: string[] = [];
searchingData.forEach((data) => {
if (!uniqueList.includes(data.ds_type)) {
uniqueList.push(data.ds_type);
}
});
return uniqueList;
};
const nameChangeHandler = (event: React.ChangeEvent<{ value: string }>) => {
setDashboardDetails({
...dashboardDetails,
name: (event.target as HTMLInputElement).value,
});
setUpdate(true);
};
useEffect(() => {
if (configure === true) {
dashboardList?.ListDashboard.forEach(
(dashboardDetail: ListDashboardResponse) => {
if (dashboardDetail.db_id === dashboardID) {
setDashboardDetails({
...dashboardDetails,
name: dashboardDetail.db_name,
dataSourceType: dashboardDetail.ds_type,
dashboardType: dashboardDetail.db_type,
dataSourceID: dashboardDetail.ds_id,
agentID: dashboardDetail.cluster_id,
dataSourceName: dashboardDetail.ds_name,
agentName: dashboardDetail.cluster_name,
information:
PreConfiguredDashboards[
selectedDashboard.selectedDashboardTemplateID ?? 0
].information,
panelGroupMap:
PreConfiguredDashboards[
selectedDashboard.selectedDashboardTemplateID ?? 0
].panelGroupMap ?? [],
panelGroups: dashboardDetail.panel_groups,
});
setUpdate(true);
}
}
);
}
}, [dashboardList]);
useEffect(() => {
if (update === true) {
CallbackToSetVars(dashboardDetails);
setUpdate(false);
}
}, [update]);
return (
<div>
<div className={classes.root}>
<Typography className={classes.heading}>
<strong>Dashboard Information</strong>
</Typography>
<div className={classes.flexDisplay}>
<div className={classes.inputDivField}>
<InputField
label="Name"
data-cy="inputDashboardName"
variant={
validateTextEmpty(dashboardDetails.name) ? 'error' : 'primary'
}
onChange={nameChangeHandler}
value={dashboardDetails.name}
/>
</div>
<div className={classes.inputDivField}>
<InputField
label="Dashboard Type"
data-cy="inputDashboardType"
variant={
validateTextEmpty(dashboardDetails.dashboardType)
? 'error'
: 'primary'
}
disabled
value={dashboardDetails.dashboardType}
/>
</div>
</div>
<div className={classes.flexDisplay}>
<div className={classes.inputDiv}>
<FormControl
variant="outlined"
className={classes.formControl}
color="primary"
focused
>
<InputLabel className={classes.selectText}>
Data Source Type
</InputLabel>
<Select
value={dashboardDetails.dataSourceType}
onChange={(event) => {
setDashboardDetails({
...dashboardDetails,
dataSourceType: event.target.value as string,
});
setUpdate(true);
}}
label="Data Source Type"
className={classes.selectText}
>
{getDataSourceType(dataSourceList?.ListDataSource ?? []).map(
(dataSourceType: string) => (
<MenuItem
key={`${dataSourceType}-kubernetesDashboard-form`}
value={dataSourceType}
>
{dataSourceType}
</MenuItem>
)
)}
</Select>
</FormControl>
</div>
<div className={classes.inputDiv}>
<FormControl
variant="outlined"
className={classes.formControl}
color="primary"
focused
>
<InputLabel className={classes.selectText}>
Data Source
</InputLabel>
<Select
value={dashboardDetails.dataSourceID}
onChange={(event) => {
let selectedDataSourceName: string = '';
dataSourceList?.ListDataSource.filter((datasource) => {
return (
datasource.ds_type === dashboardDetails.dataSourceType
);
}).forEach((dataSource: ListDataSourceResponse) => {
if (dataSource.ds_id === (event.target.value as string)) {
selectedDataSourceName = dataSource.ds_name;
}
});
setDashboardDetails({
...dashboardDetails,
dataSourceID: event.target.value as string,
dataSourceName: selectedDataSourceName,
});
setUpdate(true);
}}
label="Data Source"
className={classes.selectText}
>
{(dataSourceList?.ListDataSource ?? []).map(
(dataSource: ListDataSourceResponse) => (
<MenuItem key={dataSource.ds_id} value={dataSource.ds_id}>
{dataSource.ds_name}
</MenuItem>
)
)}
</Select>
</FormControl>
</div>
</div>
<div className={classes.inputDiv}>
<FormControl
variant="outlined"
className={classes.formControlSingle}
color="primary"
focused
>
<InputLabel className={classes.selectText}>Agent</InputLabel>
<Select
value={dashboardDetails.agentID}
onChange={(event) => {
let selectedAgentName: string = '';
agentList?.getCluster.forEach((agent: Cluster) => {
if (agent.cluster_id === (event.target.value as string)) {
selectedAgentName = agent.cluster_name;
}
});
setDashboardDetails({
...dashboardDetails,
agentID: event.target.value as string,
agentName: selectedAgentName,
});
setUpdate(true);
}}
label="Agent"
className={classes.selectText}
>
{(agentList?.getCluster ?? [])
.filter((cluster) => {
return (
cluster.is_active &&
cluster.is_cluster_confirmed &&
cluster.is_registered
);
})
.map((agent: Cluster) => (
<MenuItem key={agent.cluster_id} value={agent.cluster_id}>
{agent.cluster_name}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<Divider variant="middle" className={classes.horizontalLine} />
<div className={classes.flexDisplayInformation}>
<div className={classes.inputDivInformationHead}>
<Typography
className={classes.headingHighlighted}
align="center"
variant="subtitle1"
>
<strong>{dashboardDetails.dashboardType}</strong>
</Typography>
</div>
<div className={classes.inputDivInformationContent}>
<Typography variant="body1" align="center">
{dashboardDetails.information}
</Typography>
</div>
</div>
<Divider variant="middle" className={classes.horizontalLine} />
<div className={classes.flexDisplayInformation}>
<div className={classes.inputDivInformationHead}>
<Typography
className={classes.headingHighlighted}
align="center"
variant="subtitle1"
>
<strong>
{t(
'analyticsDashboardViews.kubernetesDashboard.form.dashboardPanels'
)}
</strong>
</Typography>
</div>
<div className={classes.inputDivInformationContent}>
{dashboardDetails.panelGroupMap.map(
(panelGroup: PanelGroupMap, index: number) => (
<div
key={`${panelGroup.groupName}-kubernetesDashboard-form`}
className={classes.panelGroup}
>
<div className={classes.panelGroupHead}>
<Typography
variant="body1"
align="center"
display="inline"
className={classes.panelGroupName}
>
{index + 1}
{`. `}
{panelGroup.groupName}
</Typography>
</div>
{panelGroup.panels.map((panel: string, index: number) => (
<Typography
key={`${panel}-kubernetesDashboard`}
variant="body1"
align="left"
className={classes.groupPanelBodyText}
>
&emsp;{index + 1}
{`. `}
{panel}
</Typography>
))}
</div>
)
)}
</div>
</div>
</div>
</div>
);
};
export default ConfigureDashboard;

View File

@ -1,138 +0,0 @@
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles((theme) => ({
root: {
background: theme.palette.background.paper,
height: '100%',
width: '100%',
border: 1,
borderRadius: 3,
paddingTop: theme.spacing(4),
paddingBottom: theme.spacing(6),
paddingLeft: theme.spacing(3.125),
paddingRight: theme.spacing(3.125),
},
flexDisplay: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
alignContent: 'center',
justifyContent: 'space-between',
paddingRight: theme.spacing(2),
},
flexDisplayInformation: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
alignContent: 'center',
justifyContent: 'space-between',
paddingRight: theme.spacing(2),
},
inputDivInformationHead: {
width: '25%',
},
inputDivInformationContent: {
width: '85%',
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-around',
},
panelGroup: {
marginTop: theme.spacing(4),
marginBottom: theme.spacing(4),
marginLeft: theme.spacing(2),
marginRight: theme.spacing(2),
height: '3rem',
width: '17rem',
[theme.breakpoints.down('xl')]: {
width: '17rem',
},
[theme.breakpoints.down('lg')]: {
width: '15rem',
},
[theme.breakpoints.down('md')]: {
width: '12rem',
},
},
groupPanelBodyText: {
[theme.breakpoints.down('xl')]: {
fontSize: '0.9rem',
},
[theme.breakpoints.down('lg')]: {
fontSize: '0.75rem',
},
[theme.breakpoints.down('md')]: {
fontSize: '0.6rem',
},
},
panelGroupHead: {
marginBottom: theme.spacing(2),
},
heading: {
fontSize: '1.5rem',
marginLeft: theme.spacing(2),
marginBottom: theme.spacing(1),
},
headingHighlighted: {
fontSize: '1.2rem',
marginLeft: theme.spacing(2),
marginBottom: theme.spacing(1),
color: theme.palette.highlight,
},
panelGroupName: {
fontSize: '0.95rem',
color: theme.palette.highlight,
},
horizontalLine: {
marginTop: theme.spacing(3.5),
marginBottom: theme.spacing(3.5),
background: theme.palette.border.main,
},
inputDiv: {
width: '26rem',
},
inputDivField: {
marginTop: theme.spacing(2),
marginBottom: theme.spacing(1),
paddingLeft: theme.spacing(2),
},
// Form Select Properties
formControl: {
marginTop: theme.spacing(3.5),
height: '3rem',
minWidth: '25rem',
marginLeft: theme.spacing(2),
marginBottom: theme.spacing(1),
},
formControlSingle: {
marginTop: theme.spacing(3),
height: '3rem',
minWidth: '25rem',
marginLeft: theme.spacing(2),
marginBottom: theme.spacing(2),
},
selectText: {
height: '3.25rem',
padding: theme.spacing(0.5),
color: theme.palette.text.primary,
},
}));
export default useStyles;

View File

@ -0,0 +1,454 @@
import Snackbar from '@material-ui/core/Snackbar';
import Typography from '@material-ui/core/Typography';
import MuiAlert, { AlertProps } from '@material-ui/lab/Alert';
import { ButtonFilled, ButtonOutlined } from 'litmus-ui';
import React, { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { LitmusStepper } from '../../../../components/LitmusStepper';
import Loader from '../../../../components/Loader';
import Row from '../../../../containers/layouts/Row';
import { DashboardDetails } from '../../../../models/dashboardsData';
import { ListDataSourceResponse } from '../../../../models/graphql/dataSourceDetails';
import useActions from '../../../../redux/actions';
import * as AlertActions from '../../../../redux/actions/alert';
import { RootState } from '../../../../redux/reducers';
import { getProjectRole } from '../../../../utils/getSearchParams';
import ChooseADashboardType from '../Steps/ChooseADashboardType';
import ConfigureDashboardMetadata from '../Steps/ConfigureDashboardMetadata';
import SelectTheMetrics from '../Steps/SelectTheMetrics';
import TuneTheQueries from '../Steps/TuneTheQueries';
import useStyles from './styles';
interface ChildRef {
onNext: () => void;
}
interface AlertBoxProps {
message: string;
}
function Alert(props: AlertProps) {
return <MuiAlert elevation={6} variant="filled" {...props} />;
}
interface DashboardStepperProps {
configure: boolean;
activePanelID: string;
existingDashboardVars: DashboardDetails;
dataSourceList: ListDataSourceResponse[];
}
const DashboardStepper: React.FC<DashboardStepperProps> = ({
configure,
activePanelID,
existingDashboardVars,
dataSourceList,
}) => {
const classes = useStyles();
const { t } = useTranslation();
const stepsArray: string[] = [
`${t('analyticsDashboard.applicationDashboards.stepper.steps.step1')}`,
`${t('analyticsDashboard.applicationDashboards.stepper.steps.step2')}`,
`${t('analyticsDashboard.applicationDashboards.stepper.steps.step3')}`,
`${t('analyticsDashboard.applicationDashboards.stepper.steps.step4')}`,
];
const childRef = useRef<ChildRef>();
const [loading, setLoading] = React.useState<boolean>(false);
const [activeStep, setActiveStep] = React.useState<number>(
configure && activePanelID !== '' ? 1 : 0
);
const [dashboardVars, setDashboardVars] = React.useState<DashboardDetails>({
id: !configure ? '' : existingDashboardVars.id ?? '',
name: !configure ? '' : existingDashboardVars.name ?? '',
dashboardTypeID: !configure
? ''
: existingDashboardVars.dashboardTypeID ?? '',
dashboardTypeName: !configure
? ''
: existingDashboardVars.dashboardTypeName ?? '',
dataSourceType: !configure
? ''
: existingDashboardVars.dataSourceType ?? '',
dataSourceID: !configure
? dataSourceList.length !== 0
? dataSourceList[0].ds_id
: ''
: existingDashboardVars.dataSourceID ?? '',
dataSourceURL: !configure
? dataSourceList.length !== 0
? dataSourceList[0].ds_url
: ''
: existingDashboardVars.dataSourceURL ?? '',
chaosEventQueryTemplate: !configure
? ''
: existingDashboardVars.chaosEventQueryTemplate ?? '',
chaosVerdictQueryTemplate: !configure
? ''
: existingDashboardVars.chaosVerdictQueryTemplate ?? '',
agentID: !configure ? '' : existingDashboardVars.agentID ?? '',
information: !configure ? '' : existingDashboardVars.information ?? '',
panelGroupMap: !configure ? [] : existingDashboardVars.panelGroupMap ?? [],
panelGroups: !configure ? [] : existingDashboardVars.panelGroups ?? [],
selectedPanelGroupMap: [],
applicationMetadataMap: !configure
? []
: existingDashboardVars.applicationMetadataMap ?? [],
});
const [disabledNext, setDisabledNext] = React.useState<boolean>(true);
let steps = stepsArray;
if (configure) {
steps = steps.filter(
(step: string) =>
step !==
`${t(
'analyticsDashboard.applicationDashboards.stepper.steps.step1'
)}` &&
step !==
`${t('analyticsDashboard.applicationDashboards.stepper.steps.step3')}`
);
} else if (dashboardVars.dashboardTypeID === 'custom') {
steps = steps.filter(
(step: string) =>
step !==
`${t('analyticsDashboard.applicationDashboards.stepper.steps.step3')}`
);
}
// Checks if the button is in loading state or not
const isButtonLoading = (status: boolean) => setLoading(status);
const handleNext = () => {
if (childRef.current && childRef.current.onNext()) {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
}
};
function getStepContent(
stepIndex: number,
childRef: React.MutableRefObject<ChildRef | undefined>
): React.ReactNode {
switch (stepIndex) {
case !configure ? 0 : -1:
return <ChooseADashboardType ref={childRef} handleNext={handleNext} />;
case !configure ? 1 : 0:
return (
<ConfigureDashboardMetadata
ref={childRef}
configure={configure}
dashboardVars={dashboardVars}
dataSourceList={dataSourceList}
handleMetadataUpdate={(dashboardMetadata: DashboardDetails) => {
setDashboardVars({
...dashboardVars,
id: dashboardMetadata.id ?? '',
name: dashboardMetadata.name ?? '',
dashboardTypeID: dashboardMetadata.dashboardTypeID ?? '',
dashboardTypeName: dashboardMetadata.dashboardTypeName ?? '',
dataSourceType: dashboardMetadata.dataSourceType ?? '',
dataSourceID: dashboardMetadata.dataSourceID ?? '',
dataSourceURL: dashboardMetadata.dataSourceURL ?? '',
chaosEventQueryTemplate:
dashboardMetadata.chaosEventQueryTemplate ?? '',
chaosVerdictQueryTemplate:
dashboardMetadata.chaosVerdictQueryTemplate ?? '',
agentID: dashboardMetadata.agentID ?? '',
information: dashboardMetadata.information ?? '',
panelGroupMap: dashboardMetadata.panelGroupMap ?? [],
panelGroups: dashboardMetadata.panelGroups ?? [],
applicationMetadataMap:
dashboardMetadata.applicationMetadataMap ?? [],
});
}}
setDisabledNext={(next: boolean) => {
setDisabledNext(next);
}}
/>
);
case !configure
? dashboardVars.dashboardTypeID !== 'custom'
? 2
: -1
: -1:
return (
<SelectTheMetrics
ref={childRef}
dashboardVars={dashboardVars}
handleMetricsUpdate={(dashboardMetrics: DashboardDetails) => {
setDashboardVars({
...dashboardVars,
selectedPanelGroupMap:
dashboardMetrics.selectedPanelGroupMap ?? [],
});
}}
setDisabledNext={(next: boolean) => {
setDisabledNext(next);
}}
/>
);
case !configure
? dashboardVars.dashboardTypeID !== 'custom'
? 3
: 2
: 1:
return (
<TuneTheQueries
ref={childRef}
configure={configure}
isLoading={isButtonLoading}
activeEditPanelID={activePanelID}
dashboardVars={dashboardVars}
/>
);
default:
return <ChooseADashboardType ref={childRef} handleNext={handleNext} />;
}
}
const isAlertOpen = useSelector(
(state: RootState) => state.alert.isAlertOpen
);
const alert = useActions(AlertActions);
const handleBack = () => {
if (activeStep === 1 && !configure) {
setDashboardVars({
id: '',
name: '',
dashboardTypeID: '',
dashboardTypeName: '',
dataSourceType: '',
dataSourceID: '',
dataSourceURL: '',
chaosEventQueryTemplate: '',
chaosVerdictQueryTemplate: '',
agentID: '',
information: '',
panelGroupMap: [],
panelGroups: [],
selectedPanelGroupMap: [],
applicationMetadataMap: [],
});
}
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};
/**
Control Buttons
------------------------------------------------------------------------------
When active step is zero (First Step) there won't be a Back button
When active step is the last step in the stepper the button will change to Finish
All steps in the middle will have next and back buttons
* */
const ControlButton: React.FC = () => {
return (
<>
{activeStep === steps.length - 1 ? ( // Show Save changes button at Bottom for Last Step
<div
className={classes.headerButtonWrapper}
aria-label="buttons"
style={{ width: 'fit-content' }}
>
{!loading && (
<ButtonOutlined
onClick={() => handleBack()}
style={{ marginRight: '1rem' }}
>
<Typography>
{t(
'analyticsDashboard.applicationDashboards.stepper.buttons.back'
)}
</Typography>
</ButtonOutlined>
)}
<ButtonFilled disabled={loading} onClick={() => handleNext()}>
{!loading && (
<img
src="/icons/save-changes.svg"
alt="Info icon"
className={classes.icon}
/>
)}
<Typography className={classes.buttonText}>
{!loading
? `${t(
'analyticsDashboard.applicationDashboards.stepper.buttons.saveChanges'
)}`
: configure
? `${t(
'analyticsDashboard.applicationDashboards.stepper.buttons.status.updating'
)}`
: `${t(
'analyticsDashboard.applicationDashboards.stepper.buttons.status.creating'
)}`}
</Typography>
{loading && <Loader size={20} />}
</ButtonFilled>
</div>
) : (activeStep !== 0 && !configure) ||
(activeStep === 0 && configure) ? ( // Apply headerButtonWrapper style for top button's div
<div className={classes.headerButtonWrapper} aria-label="buttons">
{!(activeStep === 0 && configure) && (
<ButtonOutlined
onClick={() => handleBack()}
style={{ marginRight: '1rem' }}
>
<Typography>
{t(
'analyticsDashboard.applicationDashboards.stepper.buttons.back'
)}
</Typography>
</ButtonOutlined>
)}
<ButtonFilled onClick={() => handleNext()} disabled={disabledNext}>
<Typography>
{t(
'analyticsDashboard.applicationDashboards.stepper.buttons.next'
)}
</Typography>
</ButtonFilled>
</div>
) : (
<></>
)}
</>
);
};
/**
Alert
------------------------------------------------------------------------------
Displays a snackbar with the appropriate message whenever a condition is not satisfied
* */
const AlertBox: React.FC<AlertBoxProps> = ({ message }) => {
return (
<div>
{isAlertOpen ? (
<Snackbar
open={isAlertOpen}
autoHideDuration={6000}
onClose={() => alert.changeAlertState(false)}
>
<Alert
onClose={() => alert.changeAlertState(false)}
severity="error"
>
{message}
</Alert>
</Snackbar>
) : (
<></>
)}
</div>
);
};
function getAlertMessage(stepNumber: number) {
switch (stepNumber) {
case !configure ? 0 : -1:
if (getProjectRole() === 'Viewer') {
return t(
'analyticsDashboard.applicationDashboards.stepper.errors.step1.messageViewer'
);
}
return t(
'analyticsDashboard.applicationDashboards.stepper.errors.step1.message'
);
case !configure ? 1 : 0:
return t(
'analyticsDashboard.applicationDashboards.stepper.errors.step2.messageViewer'
);
case !configure
? dashboardVars.dashboardTypeID !== 'custom'
? 2
: -1
: -1:
return t(
'analyticsDashboard.applicationDashboards.stepper.errors.step3.message'
);
case !configure
? dashboardVars.dashboardTypeID !== 'custom'
? 3
: 2
: 1:
return configure
? t(
'analyticsDashboard.applicationDashboards.stepper.errors.step4.messageConfigure'
)
: t(
'analyticsDashboard.applicationDashboards.stepper.errors.step4.messageCreate'
);
default:
return '';
}
}
useEffect(() => {
if (configure) {
setDashboardVars({
...existingDashboardVars,
});
}
}, [existingDashboardVars]);
return (
<div className={classes.root}>
{/* Alert */}
<AlertBox message={getAlertMessage(activeStep)} />
{/* Header */}
<div className={classes.headWrapper}>
<Row justifyContent="space-between">
<Typography className={classes.header}>
{!configure
? t('analyticsDashboard.applicationDashboards.createHeader')
: `${t(
'analyticsDashboard.applicationDashboards.configureHeader'
)} / ${dashboardVars.name}`}
</Typography>
<ControlButton />
</Row>
</div>
<br />
{/* Stepper */}
<LitmusStepper
steps={steps}
activeStep={activeStep}
handleBack={handleBack}
loader={loading}
hideNext={!configure ? !activeStep : false}
disableNext={
configure && activeStep === steps.length - 1 ? loading : disabledNext
}
handleNext={() => handleNext()}
finishAction={() => {}}
finishButtonText={
<>
{!loading && (
<img
src="/icons/save-changes.svg"
alt="Info icon"
className={classes.icon}
/>
)}
<Typography>
{!loading
? `${t(
'analyticsDashboard.applicationDashboards.stepper.buttons.saveChanges'
)}`
: configure
? `${t(
'analyticsDashboard.applicationDashboards.stepper.buttons.status.updating'
)}`
: `${t(
'analyticsDashboard.applicationDashboards.stepper.buttons.status.creating'
)}`}
</Typography>
</>
}
>
{getStepContent(activeStep, childRef)}
</LitmusStepper>
</div>
);
};
export default DashboardStepper;

View File

@ -0,0 +1,39 @@
import { makeStyles, Theme } from '@material-ui/core/styles';
const useStyles = makeStyles((theme: Theme) => ({
root: {
width: '97.5%',
margin: '0 auto',
[theme.breakpoints.up('lg')]: {
width: '98%',
margin: theme.spacing(2, 'auto'),
},
},
// Header
headWrapper: {
margin: theme.spacing(0.5, 'auto'),
},
header: {
fontSize: '2.25rem',
fontWeight: 500,
lineHeight: '130%',
},
headerButtonWrapper: {
display: 'flex',
width: 'fit-content',
justifyContent: 'space-between',
},
// Stepper
icon: {
marginRight: theme.spacing(1),
width: '1rem',
},
// Bottom
buttonText: {
paddingRight: theme.spacing(1),
},
}));
export default useStyles;

View File

@ -0,0 +1,42 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/no-static-element-interactions */
import { Typography } from '@material-ui/core';
import React from 'react';
import { DashboardData } from '../../../../../../models/dashboardsData';
import useStyles from './styles';
const CardContent: React.FC<DashboardData> = ({
typeName,
urlToIcon,
handleClick,
information,
}) => {
const classes = useStyles();
return (
<div className={classes.card}>
<div className={classes.cardContent} onClick={handleClick ?? (() => {})}>
{urlToIcon ? (
<div className={classes.cardMedia}>
<img src={urlToIcon} alt="dashboard logo" />
</div>
) : (
<div className={classes.noImage}>Image</div>
)}
<div className={classes.meta}>
<Typography data-cy="dashboardName" className={classes.title}>
{typeName}
</Typography>
{information ? (
<Typography className={classes.description}>
{information}
</Typography>
) : (
<span />
)}
</div>
</div>
</div>
);
};
export default CardContent;

View File

@ -0,0 +1,120 @@
import { Typography } from '@material-ui/core';
import { ButtonOutlined, Modal } from 'litmus-ui';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { DashboardData } from '../../../../../../models/dashboardsData';
import useActions from '../../../../../../redux/actions';
import * as DashboardActions from '../../../../../../redux/actions/dashboards';
import DashboardCard from './index';
import useStyles from './styles';
import UploadJSON from './UploadDashboard';
interface DashboardCardsProps {
dashboards: DashboardData[];
handleClick: () => void;
generateAlert: () => void;
}
const DashboardCards: React.FC<DashboardCardsProps> = ({
dashboards,
handleClick,
generateAlert,
}) => {
const classes = useStyles();
const { t } = useTranslation();
const dashboard = useActions(DashboardActions);
// Function to fetch json
const fetchJson = (link: string) => {
fetch(link)
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP status ${response.status}`);
}
response.text().then((jsonText) => {
dashboard.selectDashboard({
dashboardJSON: JSON.parse(jsonText),
});
handleClick();
});
})
.catch((err) => {
console.error(err);
generateAlert();
});
};
const [upload, setUpload] = React.useState(false);
return (
<div>
<div className={classes.root}>
{dashboards &&
dashboards.map((d: DashboardData) => (
<div key={d.dashboardTypeID} data-cy="dashboardCard">
<DashboardCard
key={d.dashboardTypeID}
dashboardTypeID={d.dashboardTypeID}
typeName={d.typeName}
urlToIcon={d.urlToIcon}
handleClick={() => {
if (
d.dashboardTypeID !== 'custom' &&
d.dashboardTypeID !== 'upload'
) {
fetchJson(d.urlToDashboard ?? '');
} else if (d.dashboardTypeID === 'custom') {
dashboard.selectDashboard({
dashboardJSON: null,
});
handleClick();
} else if (d.dashboardTypeID === 'upload') {
setUpload(true);
}
}}
information={d.information}
/>
</div>
))}
</div>
{upload && (
<Modal
open
onClose={() => {
setUpload(false);
}}
modalActions={
<ButtonOutlined
className={classes.closeButton}
onClick={() => {
setUpload(false);
}}
>
&#x2715;
</ButtonOutlined>
}
width="50%"
height="fit-content"
>
<div className={classes.modal}>
<Typography className={classes.modalHeading} align="left">
{t(
'analyticsDashboard.applicationDashboards.chooseADashboardType.uploadModal.heading'
)}
</Typography>
<UploadJSON
successHandler={() => handleClick()}
errorHandler={() => {
generateAlert();
}}
/>
</div>
</Modal>
)}
</div>
);
};
export default DashboardCards;

View File

@ -0,0 +1,157 @@
/* eslint-disable jsx-a11y/label-has-associated-control */
import { Button, Paper, Typography } from '@material-ui/core';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ApplicationDashboard } from '../../../../../../../models/redux/dashboards';
import useActions from '../../../../../../../redux/actions';
import * as DashboardActions from '../../../../../../../redux/actions/dashboards';
import useStyles from './styles';
interface UploadJSONProps {
successHandler: () => void;
errorHandler: () => void;
}
const UploadJSON: React.FC<UploadJSONProps> = ({
successHandler,
errorHandler,
}) => {
const classes = useStyles();
const { t } = useTranslation();
const [uploadedJSON, setUploadedJSON] = useState('');
const [error, setError] = useState('');
const [fileName, setFileName] = useState<string | null>('');
const dashboard = useActions(DashboardActions);
// Function to handle when a File is dragged on the upload field
const handleDrag = (e: React.DragEvent<HTMLDivElement>) => {
Array.from(e.dataTransfer.files)
.filter((file) => file.name.split('.')[1] === 'json')
.forEach(async (file) => {
const readFile = await file.text();
setUploadedJSON(readFile);
setFileName(file.name);
try {
const parsedDashboard: ApplicationDashboard = JSON.parse(readFile);
dashboard.selectDashboard({
selectedDashboardID: 'upload',
dashboardJSON: parsedDashboard,
});
if (
parsedDashboard.panelGroups[0].panels[0].prom_queries[0]
.prom_query_name
) {
successHandler();
} else {
throw new Error('Invalid dashboard.');
}
} catch (err) {
setError((err as any).toString());
errorHandler();
}
});
};
// Function to handle File upload on button click
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const readFile = e.target.files && e.target.files[0];
setFileName(readFile && readFile.name);
const extension = readFile?.name.substring(
readFile.name.lastIndexOf('.') + 1
);
if (extension === 'json' && readFile) {
readFile.text().then((response) => {
setUploadedJSON(response);
try {
const parsedDashboard: ApplicationDashboard = JSON.parse(response);
dashboard.selectDashboard({
selectedDashboardID: 'upload',
dashboardJSON: parsedDashboard,
});
if (
parsedDashboard.panelGroups[0].panels[0].prom_queries[0]
.prom_query_name
) {
successHandler();
} else {
throw new Error('Invalid dashboard.');
}
} catch (err) {
setError((err as any).toString());
errorHandler();
}
});
}
};
return (
<Paper
elevation={3}
component="div"
onDragOver={(e) => {
e.preventDefault();
}}
onDrop={(e) => {
e.preventDefault();
handleDrag(e);
}}
className={classes.uploadJSONDiv}
>
{uploadedJSON === '' || error !== '' ? (
<div className={classes.uploadJSONText}>
<img
src="/icons/upload-dashboard.svg"
alt="upload json"
className={classes.uploadImage}
/>
<Typography variant="h6">
{t(
'analyticsDashboard.applicationDashboards.chooseADashboardType.uploadModal.option1'
)}
</Typography>
<Typography className={classes.orText}>
{t(
'analyticsDashboard.applicationDashboards.chooseADashboardType.uploadModal.or'
)}
</Typography>
<input
accept=".json"
style={{ display: 'none' }}
id="contained-button-file"
type="file"
onChange={(e) => {
handleFileUpload(e);
}}
/>
<label htmlFor="contained-button-file">
<label htmlFor="contained-button-file">
<Button
variant="outlined"
className={classes.uploadBtn}
component="span"
>
{t(
'analyticsDashboard.applicationDashboards.chooseADashboardType.uploadModal.option2'
)}
</Button>
</label>
</label>
</div>
) : (
<div className={classes.uploadSuccessDiv}>
<img
src="/icons/upload-success.svg"
alt="checkmark"
className={classes.uploadSuccessImg}
/>
<Typography className={classes.uploadSuccessText}>
{t(
'analyticsDashboard.applicationDashboards.chooseADashboardType.uploadModal.successMessage'
)}{' '}
{fileName}
</Typography>
</div>
)}
</Paper>
);
};
export default UploadJSON;

View File

@ -0,0 +1,67 @@
import { fade, makeStyles, Theme } from '@material-ui/core/styles';
const useStyles = makeStyles((theme: Theme) => ({
// Upload button styles
uploadJSONDiv: {
width: '85%',
padding: theme.spacing(3.75),
border: `1px dashed ${theme.palette.border.main}`,
margin: 'auto',
marginTop: theme.spacing(1),
borderRadius: theme.spacing(1.25),
backgroundColor: theme.palette.cards.header,
},
uploadJSONText: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'space-between',
width: '31.25rem',
margin: 'auto',
paddingTop: theme.spacing(1),
},
uploadImage: {
marginBottom: theme.spacing(2.5),
},
orText: {
margin: theme.spacing(1.25, 0),
},
uploadBtn: {
textTransform: 'none',
width: 'fit-content',
fontSize: '0.7rem',
height: '2.8125rem',
border: `2px solid ${theme.palette.primary.light}`,
borderRadius: '0.25rem',
'&:hover': {
backgroundColor: theme.palette.background.paper,
borderColor: (props) =>
props !== true ? theme.palette.primary.light : '',
boxShadow: (props) =>
props !== true
? `${fade(theme.palette.primary.light, 0.5)} 0 0.3rem 0.4rem 0`
: 'none',
},
},
uploadSuccessDiv: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
maxWidth: '31.25rem',
margin: '0 auto',
paddingTop: theme.spacing(1.875),
},
uploadSuccessImg: {
width: '3.125rem',
height: '3.125rem',
verticalAlign: 'middle',
paddingBottom: theme.spacing(1),
},
uploadSuccessText: {
display: 'inline-block',
fontSize: '1rem',
margin: theme.spacing(0, 0, 1.25, 2.5),
},
}));
export default useStyles;

View File

@ -1,5 +1,5 @@
import React from 'react';
import { DashboardData } from '../../../../models/dashboardsData';
import { DashboardData } from '../../../../../../models/dashboardsData';
import CardContainer from './CardContainer';
import CardContent from './CardContent';

View File

@ -0,0 +1,86 @@
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles((theme) => ({
root: {
display: 'grid',
gridTemplateColumns: '1fr 1fr',
[theme.breakpoints.up('md')]: {
flexDirection: 'row',
},
background: theme.palette.background.paper,
padding: theme.spacing(0, 4),
},
// CardContent
card: {
background: theme.palette.cards.background,
borderRadius: '3px',
height: '7.75rem',
margin: theme.spacing(1),
cursor: 'pointer',
border: `1px solid ${theme.palette.border.main}`,
'&:hover': {
border: `1px solid ${theme.palette.primary.main}`,
},
},
// CARD MEDIA
cardMedia: {
margin: 'auto 0',
'& img': {
height: '3.75rem',
marginLeft: theme.spacing(3),
},
},
meta: {
margin: theme.spacing(2.5, 0, 2.5, 3),
},
// CARD CONTENT
cardContent: {
display: 'grid',
gridTemplateColumns: '0.2fr 0.8fr',
},
title: {
letterSpacing: '0.1142px',
fontWeight: 500,
fontSize: '1.125rem',
lineHeight: '1.375rem',
textAlign: 'left',
},
description: {
fontSize: '0.75rem',
lineHeight: '150%',
textAlign: 'left',
marginTop: theme.spacing(1),
},
noImage: {
width: '100%',
height: '5rem',
backgroundColor: theme.palette.background.paper,
},
modal: {
padding: theme.spacing(3.5, 0, 8.5),
},
modalHeading: {
margin: theme.spacing(4.5, 0, 6.5),
paddingLeft: theme.spacing(6.5),
fontSize: '1.5rem',
},
closeButton: {
borderColor: theme.palette.border.main,
color: theme.palette.border.main,
padding: theme.spacing(0.5),
minWidth: '2.5rem',
},
}));
export default useStyles;

View File

@ -0,0 +1,59 @@
import { Typography } from '@material-ui/core';
import React, { forwardRef, useImperativeHandle } from 'react';
import { useTranslation } from 'react-i18next';
import DashboardList from '../../../../../components/PreconfiguredDashboards/data';
import useActions from '../../../../../redux/actions';
import * as AlertActions from '../../../../../redux/actions/alert';
import { getProjectRole } from '../../../../../utils/getSearchParams';
import DashboardCards from './Cards/DashBoardCards';
import useStyles from './styles';
interface ChooseADashboardTypeProps {
handleNext: () => void;
}
const ChooseADashboardType = forwardRef(
({ handleNext }: ChooseADashboardTypeProps, ref) => {
const classes = useStyles();
const { t } = useTranslation();
const alert = useActions(AlertActions);
function onNext() {
if (getProjectRole() === 'Viewer') {
alert.changeAlertState(true);
return false;
}
return true;
}
useImperativeHandle(ref, () => ({
onNext,
}));
return (
<div>
<Typography className={classes.heading}>
{t(
'analyticsDashboard.applicationDashboards.chooseADashboardType.header'
)}
</Typography>
<Typography className={classes.description}>
{t(
'analyticsDashboard.applicationDashboards.chooseADashboardType.description'
)}
</Typography>
<div className={classes.cards}>
<DashboardCards
dashboards={DashboardList}
handleClick={handleNext}
generateAlert={() => {
alert.changeAlertState(true);
}}
/>
</div>
</div>
);
}
);
export default ChooseADashboardType;

View File

@ -0,0 +1,26 @@
import { makeStyles, Theme } from '@material-ui/core/styles';
const useStyles = makeStyles((theme: Theme) => ({
heading: {
fontWeight: 500,
fontSize: '1.5rem',
lineHeight: '1.8125rem',
padding: theme.spacing(0, 5),
},
description: {
width: '90%',
margin: theme.spacing(0.5, 0, 2),
fontSize: '1rem',
lineHeight: '150%',
letterSpacing: '0.1176px',
padding: theme.spacing(1, 5),
},
cards: {
background: theme.palette.background.paper,
paddingBottom: theme.spacing(3),
},
}));
export default useStyles;

View File

@ -0,0 +1,49 @@
import { constants } from '../../../../../../constants';
export default [
{
group: '',
version: constants.v1,
resource: constants.pods,
},
{
group: '',
version: constants.v1,
resource: constants.services,
},
{
group: '',
version: constants.v1,
resource: constants.nodes,
},
{
group: constants.apps,
version: constants.v1,
resource: constants.deployments,
},
{
group: constants.apps,
version: constants.v1,
resource: constants.statefulsets,
},
{
group: constants.apps,
version: constants.v1,
resource: constants.daemonsets,
},
{
group: constants.apps,
version: constants.v1,
resource: constants.replicasets,
},
{
group: constants.openshift,
version: constants.v1,
resource: constants.deploymentconfigs,
},
{
group: constants.argoproj,
version: constants.v1alpha1,
resource: constants.rollouts,
},
];

View File

@ -0,0 +1,597 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useQuery, useSubscription } from '@apollo/client';
import {
FormControl,
InputLabel,
MenuItem,
Select,
Typography,
} from '@material-ui/core';
import { AutocompleteChipInput, InputField } from 'litmus-ui';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { GET_CLUSTER, KUBE_OBJ } from '../../../../../../graphql';
import { DashboardDetails } from '../../../../../../models/dashboardsData';
import {
Cluster,
Clusters,
ClusterVars,
} from '../../../../../../models/graphql/clusterData';
import {
GVRRequest,
KubeObjData,
KubeObjRequest,
KubeObjResource,
KubeObjResponse,
} from '../../../../../../models/graphql/createWorkflowData';
import {
ApplicationMetadata,
Resource,
} from '../../../../../../models/graphql/dashboardsDetails';
import { ListDataSourceResponse } from '../../../../../../models/graphql/dataSourceDetails';
import useActions from '../../../../../../redux/actions';
import * as DashboardActions from '../../../../../../redux/actions/dashboards';
import { RootState } from '../../../../../../redux/reducers';
import { getProjectID } from '../../../../../../utils/getSearchParams';
import { validateTextEmpty } from '../../../../../../utils/validate';
import gvrList from './data';
import useStyles from './styles';
interface DashboardMetadataFormProps {
dashboardVars: DashboardDetails;
dataSourceList: ListDataSourceResponse[];
configure: boolean;
CallbackToSetVars: (vars: DashboardDetails) => void;
setDisabledNext: (next: boolean) => void;
}
interface Option {
name: string;
[index: string]: any;
}
const DashboardMetadataForm: React.FC<DashboardMetadataFormProps> = ({
dashboardVars,
dataSourceList,
configure,
CallbackToSetVars,
setDisabledNext,
}) => {
const classes = useStyles();
const { t } = useTranslation();
const projectID = getProjectID();
const dashboard = useActions(DashboardActions);
const selectedDashboard = useSelector(
(state: RootState) => state.selectDashboard
);
const getSelectedApps = (dashboardJSON: any) => {
dashboard.selectDashboard({
selectedDashboardID: '',
});
return dashboardJSON.applicationMetadataMap;
};
const [dashboardDetails, setDashboardDetails] = useState<DashboardDetails>({
id: !configure ? '' : dashboardVars.id ?? '',
name: !configure
? selectedDashboard.dashboardJSON
? selectedDashboard.dashboardJSON.name
: 'custom'
: dashboardVars.name ?? '',
dashboardTypeID: !configure
? selectedDashboard.dashboardJSON
? selectedDashboard.dashboardJSON.dashboardID
: 'custom'
: dashboardVars.dashboardTypeID ?? '',
dashboardTypeName: !configure
? selectedDashboard.dashboardJSON
? selectedDashboard.dashboardJSON.name
: 'Custom'
: dashboardVars.dashboardTypeName ?? '',
dataSourceType: !configure
? 'Prometheus'
: dashboardVars.dataSourceType ?? '',
dataSourceID: dashboardVars.dataSourceID ?? '',
dataSourceURL: dashboardVars.dataSourceURL ?? '',
chaosEventQueryTemplate: !configure
? selectedDashboard.dashboardJSON
? selectedDashboard.dashboardJSON.chaosEventQueryTemplate
: 'litmuschaos_awaited_experiments{job="chaos-exporter"}'
: dashboardVars.chaosEventQueryTemplate ?? '',
chaosVerdictQueryTemplate: !configure
? selectedDashboard.dashboardJSON
? selectedDashboard.dashboardJSON.chaosVerdictQueryTemplate
: 'litmuschaos_experiment_verdict{job="chaos-exporter"}'
: dashboardVars.chaosVerdictQueryTemplate ?? '',
agentID: dashboardVars.agentID ?? '',
information: !configure
? selectedDashboard.dashboardJSON
? selectedDashboard.dashboardJSON.information
: 'Customized dashboard'
: dashboardVars.information ?? '',
panelGroupMap: dashboardVars.panelGroupMap ?? [],
panelGroups: dashboardVars.panelGroups ?? [],
applicationMetadataMap: !configure
? selectedDashboard.selectedDashboardID !== 'upload'
? dashboardVars.applicationMetadataMap
: getSelectedApps(selectedDashboard.dashboardJSON)
: dashboardVars.applicationMetadataMap ?? [],
});
const [update, setUpdate] = useState(false);
const [availableApplicationMetadataMap, setAvailableApplicationMetadataMap] =
useState<ApplicationMetadata[]>([]);
const [kubeObjInput, setKubeObjInput] = useState<GVRRequest>({
group: '',
version: 'v1',
resource: 'pods',
});
const [selectedNamespaceList, setSelectedNamespaceList] = useState<
Array<Option>
>([]);
// Apollo query to get the agent data
const { data: agentList } = useQuery<Clusters, ClusterVars>(GET_CLUSTER, {
variables: { project_id: projectID },
fetchPolicy: 'cache-and-network',
});
/**
* GraphQL subscription to fetch the KubeObjData from the server
*/
const { data: kubeObjectData } = useSubscription<
KubeObjResponse,
KubeObjRequest
>(KUBE_OBJ, {
variables: {
data: {
cluster_id: dashboardDetails.agentID ?? '',
object_type: 'kubeobject',
kube_obj_request: {
group: kubeObjInput.group,
version: kubeObjInput.version,
resource: kubeObjInput.resource,
},
},
},
onSubscriptionComplete: () => {
const newAvailableApplicationMetadataMap: ApplicationMetadata[] = [];
try {
const kubeData: KubeObjData[] = JSON.parse(
kubeObjectData?.getKubeObject.kube_obj ?? ''
);
kubeData.forEach((obj: KubeObjData) => {
const newAvailableApplicationMetadata: ApplicationMetadata = {
namespace: obj.namespace,
applications: [
{
kind: kubeObjInput.resource,
names: [],
},
],
};
if (obj.data != null) {
obj.data.forEach((objData: KubeObjResource) => {
if (objData.name != null) {
newAvailableApplicationMetadata.applications[0].names.push(
objData.name
);
}
});
}
newAvailableApplicationMetadataMap.push(
newAvailableApplicationMetadata
);
});
} catch (err) {
console.error(err);
}
setAvailableApplicationMetadataMap(newAvailableApplicationMetadataMap);
},
fetchPolicy: 'network-only',
});
const nameChangeHandler = (event: React.ChangeEvent<{ value: string }>) => {
setDashboardDetails({
...dashboardDetails,
name: (event.target as HTMLInputElement).value,
});
setUpdate(true);
};
const getSelectedDsURL = (selectedDsID: string) => {
let selectedDsURL: string = '';
dataSourceList.forEach((ds) => {
if (ds.ds_id === selectedDsID) {
selectedDsURL = ds.ds_url;
}
});
return selectedDsURL;
};
useEffect(() => {
if (
dashboardDetails.name === '' ||
dashboardDetails.dashboardTypeID === '' ||
dashboardDetails.dashboardTypeName === '' ||
dashboardDetails.dataSourceType === '' ||
dashboardDetails.dataSourceID === '' ||
dashboardDetails.dataSourceURL === '' ||
dashboardDetails.chaosEventQueryTemplate === '' ||
dashboardDetails.chaosVerdictQueryTemplate === '' ||
dashboardDetails.agentID === '' ||
dashboardDetails.information === ''
) {
setDisabledNext(true);
} else if (
configure === true &&
(dashboardDetails.id === '' ||
dashboardDetails.panelGroupMap?.length === 0 ||
dashboardDetails.panelGroups?.length === 0)
) {
setDisabledNext(true);
} else {
setDisabledNext(false);
}
if (update === true) {
CallbackToSetVars(dashboardDetails);
setUpdate(false);
}
}, [update]);
useEffect(() => {
if (dashboardDetails.agentID === '' && !configure) {
const availableAgents = (agentList?.getCluster ?? []).filter(
(cluster) => {
return (
cluster.is_active &&
cluster.is_cluster_confirmed &&
cluster.is_registered
);
}
);
setDashboardDetails({
...dashboardDetails,
agentID: availableAgents.length ? availableAgents[0].cluster_id : '',
});
setUpdate(true);
}
}, [agentList]);
useEffect(() => {
if (dashboardDetails.dataSourceID === '' && !configure) {
const availableDataSources = dataSourceList.filter((dataSource) => {
return dataSource.health_status === 'Active';
});
setDashboardDetails({
...dashboardDetails,
dataSourceID: availableDataSources.length
? availableDataSources[0].ds_id
: '',
dataSourceURL: availableDataSources.length
? availableDataSources[0].ds_url
: '',
});
setUpdate(true);
}
}, [dataSourceList]);
const getAvailableApplications = () => {
const availableApplications: Array<Option> = [];
availableApplicationMetadataMap.forEach((appMetadata) => {
selectedNamespaceList.forEach((namespaceOption) => {
if (namespaceOption.name === appMetadata.namespace) {
const apps: Resource[] = appMetadata.applications.filter(
(application) => application.kind === kubeObjInput.resource
);
if (apps.length) {
apps[0].names.forEach((appName) => {
availableApplications.push({
name: `${
namespaceOption.name
} / ${kubeObjInput.resource.substring(
0,
kubeObjInput.resource.length - 1
)} / ${appName}`,
});
});
}
}
});
});
return availableApplications;
};
const getSelectedAppDetails = () => {
const options: Array<Option> = [];
dashboardDetails.applicationMetadataMap?.forEach((app) => {
app.applications.forEach((resources) => {
resources.names.forEach((name) => {
options.push({
name: `${app.namespace} / ${resources.kind} / ${name}`,
});
});
});
});
return options;
};
const getSelectedAppNamespaces = () => {
const options: Array<Option> = [];
dashboardDetails.applicationMetadataMap?.forEach((app) => {
options.push({
name: app.namespace,
});
});
return options;
};
useEffect(() => {
if (configure) {
setDashboardDetails({
...dashboardVars,
});
if (
dashboardDetails.name === '' ||
dashboardDetails.dashboardTypeID === '' ||
dashboardDetails.dashboardTypeName === '' ||
dashboardDetails.dataSourceType === '' ||
dashboardDetails.dataSourceID === '' ||
dashboardDetails.dataSourceURL === '' ||
dashboardDetails.chaosEventQueryTemplate === '' ||
dashboardDetails.chaosVerdictQueryTemplate === '' ||
dashboardDetails.agentID === '' ||
dashboardDetails.information === '' ||
dashboardDetails.panelGroupMap?.length === 0 ||
dashboardDetails.panelGroups?.length === 0
) {
setDisabledNext(true);
} else {
setDisabledNext(false);
}
}
}, [dashboardVars]);
return (
<div className={classes.root}>
<div className={classes.flexDisplay}>
<InputField
label={t(
'analyticsDashboard.applicationDashboards.configureDashboardMetadata.form.name'
)}
data-cy="inputDashboardName"
width="20rem"
variant={
validateTextEmpty(dashboardDetails.name ?? '') ? 'error' : 'primary'
}
onChange={nameChangeHandler}
value={dashboardDetails.name}
/>
<FormControl
variant="outlined"
className={classes.formControl}
color="primary"
>
<InputLabel className={classes.selectTextLabel}>
{t(
'analyticsDashboard.applicationDashboards.configureDashboardMetadata.form.agent'
)}
</InputLabel>
<Select
value={dashboardDetails.agentID}
onChange={(event) => {
setDashboardDetails({
...dashboardDetails,
agentID: event.target.value as string,
});
setUpdate(true);
}}
label={t(
'analyticsDashboard.applicationDashboards.configureDashboardMetadata.form.agent'
)}
className={classes.selectText}
>
{(agentList?.getCluster ?? [])
.filter((cluster) => {
return (
cluster.is_active &&
cluster.is_cluster_confirmed &&
cluster.is_registered
);
})
.map((agent: Cluster) => (
<MenuItem key={agent.cluster_id} value={agent.cluster_id}>
{agent.cluster_name}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div className={classes.flexDisplay}>
<FormControl
variant="outlined"
className={classes.formControl}
color="primary"
>
<InputLabel className={classes.selectTextLabel}>
{t(
'analyticsDashboard.applicationDashboards.configureDashboardMetadata.form.dataSource'
)}
</InputLabel>
<Select
value={dashboardDetails.dataSourceID}
onChange={(event) => {
setDashboardDetails({
...dashboardDetails,
dataSourceID: event.target.value as string,
dataSourceURL: getSelectedDsURL(event.target.value as string),
});
setUpdate(true);
}}
label={t(
'analyticsDashboard.applicationDashboards.configureDashboardMetadata.form.dataSource'
)}
className={classes.selectText}
>
{dataSourceList.map((dataSource: ListDataSourceResponse) => (
<MenuItem key={dataSource.ds_id} value={dataSource.ds_id}>
{dataSource.ds_name}
</MenuItem>
))}
</Select>
</FormControl>
<InputField
label={t(
'analyticsDashboard.applicationDashboards.configureDashboardMetadata.form.dashboardType'
)}
data-cy="inputDashboardType"
width="20rem"
variant={
validateTextEmpty(dashboardDetails.dashboardTypeName ?? '')
? 'error'
: 'primary'
}
disabled
value={dashboardDetails.dashboardTypeName}
/>
</div>
{(dashboardDetails.dashboardTypeID === 'custom' ||
dashboardDetails.dashboardTypeID?.startsWith('generic')) && (
<div>
<Typography className={classes.heading}>
{t(
'analyticsDashboard.applicationDashboards.configureDashboardMetadata.form.applications'
)}
</Typography>
<AutocompleteChipInput
defaultValue={getSelectedAppNamespaces()}
onChange={(event, value, reason) => {
setSelectedNamespaceList(value as Array<Option>);
}}
options={availableApplicationMetadataMap.map((value) => {
return { name: value.namespace };
})}
label={t(
'analyticsDashboard.applicationDashboards.configureDashboardMetadata.form.selectNamespaces'
)}
placeholder={`${t(
'analyticsDashboard.applicationDashboards.configureDashboardMetadata.form.addNamespace'
)}`}
disableCloseOnSelect
disableClearable={false}
limitTags={4}
className={classes.namespaceSelect}
/>
<div className={classes.appSelectFlex}>
<FormControl
variant="outlined"
className={classes.formControl}
style={{ width: '12.5rem' }}
color="primary"
>
<InputLabel className={classes.selectTextLabel}>
{t(
'analyticsDashboard.applicationDashboards.configureDashboardMetadata.form.selectApplicationType'
)}
</InputLabel>
<Select
value={kubeObjInput.resource}
onChange={(event: any) => {
setKubeObjInput(
gvrList.filter(
(gvr) => gvr.resource === (event.target.value as string)
)[0]
);
}}
label={t(
'analyticsDashboard.applicationDashboards.configureDashboardMetadata.form.selectApplicationType'
)}
className={classes.selectText}
>
{gvrList.map((gvr: GVRRequest) => (
<MenuItem key={gvr.resource} value={gvr.resource}>
{gvr.resource}
</MenuItem>
))}
</Select>
</FormControl>
<AutocompleteChipInput
defaultValue={getSelectedAppDetails()}
onChange={(event, value, reason) => {
const newSelection: ApplicationMetadata[] = [];
const selectedApps: Array<Option> = value as Array<Option>;
selectedApps.forEach((nsKindApp) => {
const selectedNs = nsKindApp.name.split('/')[0].trim();
const selectedKind = nsKindApp.name.split('/')[1].trim();
const selectedApp = nsKindApp.name.split('/')[2].trim();
let nsFound = false;
newSelection.forEach((nsMap, index) => {
if (nsMap.namespace === selectedNs) {
nsFound = true;
let kindFound = false;
newSelection[index].applications.forEach(
(kindMap, matchIndex) => {
if (kindMap.kind === selectedKind) {
kindFound = true;
newSelection[index].applications[
matchIndex
].names.push(selectedApp);
}
}
);
if (!kindFound) {
newSelection[index].applications.push({
kind: selectedKind,
names: [selectedApp],
});
}
}
});
if (!nsFound) {
newSelection.push({
namespace: selectedNs,
applications: [
{ kind: selectedKind, names: [selectedApp] },
],
});
}
});
setDashboardDetails({
...dashboardDetails,
applicationMetadataMap:
dashboardDetails.dashboardTypeID === 'custom' ||
dashboardDetails.dashboardTypeID?.startsWith('generic')
? newSelection
: [],
});
setUpdate(true);
}}
options={getAvailableApplications()}
label={t(
'analyticsDashboard.applicationDashboards.configureDashboardMetadata.form.selectApplications'
)}
placeholder={`${t(
'analyticsDashboard.applicationDashboards.configureDashboardMetadata.form.addApplication'
)}`}
disableCloseOnSelect
disableClearable={false}
limitTags={4}
style={{ width: '27.5rem' }}
/>
</div>
</div>
)}
</div>
);
};
export default DashboardMetadataForm;

View File

@ -0,0 +1,56 @@
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles((theme) => ({
root: {
background: theme.palette.background.paper,
height: '100%',
width: '100%',
border: 1,
borderRadius: 3,
padding: theme.spacing(0, 4),
},
flexDisplay: {
height: '5rem',
display: 'flex',
gap: '1.5rem',
marginLeft: theme.spacing(1),
},
heading: {
fontSize: '1rem',
fontWeight: 500,
lineHeight: '150%',
marginLeft: theme.spacing(1),
},
// Form Select Properties
formControl: {
height: '3.25rem',
width: '20rem',
},
selectTextLabel: {
color: theme.palette.border.main,
'&.Mui-focused': {
color: theme.palette.primary.main,
},
},
selectText: {
height: '3.25rem',
},
namespaceSelect: {
width: '41.5rem',
margin: theme.spacing(3.5, 1),
},
appSelectFlex: {
display: 'flex',
gap: '1.5rem',
margin: theme.spacing(0, 1),
},
}));
export default useStyles;

View File

@ -0,0 +1,73 @@
import { Typography } from '@material-ui/core';
import React, { forwardRef, useImperativeHandle } from 'react';
import { useTranslation } from 'react-i18next';
import { DashboardDetails } from '../../../../../models/dashboardsData';
import { ListDataSourceResponse } from '../../../../../models/graphql/dataSourceDetails';
import useActions from '../../../../../redux/actions';
import * as AlertActions from '../../../../../redux/actions/alert';
import { getProjectRole } from '../../../../../utils/getSearchParams';
import DashboardMetadataForm from './Form';
import useStyles from './styles';
interface ConfigureDashboardMetadataProps {
dashboardVars: DashboardDetails;
dataSourceList: ListDataSourceResponse[];
handleMetadataUpdate: (dashboardMetadata: DashboardDetails) => void;
configure: boolean;
setDisabledNext: (next: boolean) => void;
}
const ConfigureDashboardMetadata = forwardRef(
(
{
dashboardVars,
dataSourceList,
handleMetadataUpdate,
configure,
setDisabledNext,
}: ConfigureDashboardMetadataProps,
ref
) => {
const classes = useStyles();
const { t } = useTranslation();
const alert = useActions(AlertActions);
function onNext() {
if (getProjectRole() === 'Viewer') {
alert.changeAlertState(true);
return false;
}
return true;
}
useImperativeHandle(ref, () => ({
onNext,
}));
return (
<div>
<Typography className={classes.heading}>
{t(
'analyticsDashboard.applicationDashboards.configureDashboardMetadata.header'
)}
</Typography>
<Typography className={classes.description}>
{t(
'analyticsDashboard.applicationDashboards.configureDashboardMetadata.description'
)}
</Typography>
<div className={classes.metadataForm}>
<DashboardMetadataForm
dashboardVars={dashboardVars}
dataSourceList={dataSourceList}
configure={configure}
CallbackToSetVars={handleMetadataUpdate}
setDisabledNext={setDisabledNext}
/>
</div>
</div>
);
}
);
export default ConfigureDashboardMetadata;

View File

@ -0,0 +1,26 @@
import { makeStyles, Theme } from '@material-ui/core/styles';
const useStyles = makeStyles((theme: Theme) => ({
heading: {
fontWeight: 500,
fontSize: '1.5rem',
lineHeight: '1.8125rem',
padding: theme.spacing(0, 5),
},
description: {
width: '90%',
margin: theme.spacing(0.5, 0, 2),
fontSize: '1rem',
lineHeight: '150%',
letterSpacing: '0.1176px',
padding: theme.spacing(1, 5),
},
metadataForm: {
background: theme.palette.background.paper,
paddingBottom: theme.spacing(3),
},
}));
export default useStyles;

View File

@ -0,0 +1,153 @@
/* eslint-disable no-unused-expressions */
import { FormControlLabel, FormGroup, Typography } from '@material-ui/core';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { CheckBox } from '../../../../../../components/CheckBox';
import {
DashboardDetails,
PanelGroupMap,
} from '../../../../../../models/dashboardsData';
import { RootState } from '../../../../../../redux/reducers';
import useStyles from './styles';
interface SelectTheMetricsFormProps {
dashboardVars: DashboardDetails;
CallbackToSetVars: (vars: DashboardDetails) => void;
setDisabledNext: (next: boolean) => void;
generateAlert: () => void;
}
const SelectTheMetricsForm: React.FC<SelectTheMetricsFormProps> = ({
dashboardVars,
CallbackToSetVars,
setDisabledNext,
generateAlert,
}) => {
const classes = useStyles();
const { t } = useTranslation();
const selectedDashboard = useSelector(
(state: RootState) => state.selectDashboard
);
const [dashboardDetails, setDashboardDetails] = useState<DashboardDetails>({
selectedPanelGroupMap:
dashboardVars.selectedPanelGroupMap &&
dashboardVars.selectedPanelGroupMap.length
? dashboardVars.selectedPanelGroupMap
: selectedDashboard.dashboardJSON &&
selectedDashboard.dashboardJSON.panelGroupMap
? selectedDashboard.dashboardJSON.panelGroupMap.map(
(panelGroup: PanelGroupMap) => ({
groupName: panelGroup.groupName,
panels: [],
})
)
: [],
});
const [update, setUpdate] = useState(false);
const handleMetricSelect = (panel: string, index: number) => {
const selectedPanelGroupMapArray: PanelGroupMap[] =
dashboardDetails.selectedPanelGroupMap ?? [];
if (selectedPanelGroupMapArray[index].panels.includes(panel)) {
selectedPanelGroupMapArray[index].panels = selectedPanelGroupMapArray[
index
].panels.filter((selectedPanel: string) => selectedPanel !== panel);
} else {
selectedPanelGroupMapArray[index].panels.push(panel);
}
setDashboardDetails({ selectedPanelGroupMap: selectedPanelGroupMapArray });
setUpdate(true);
};
useEffect(() => {
let selectedNumberOfMetrics = 0;
dashboardDetails.selectedPanelGroupMap?.forEach((panelGroup) => {
selectedNumberOfMetrics += panelGroup.panels.length;
});
if (
!dashboardDetails.selectedPanelGroupMap?.length ||
!selectedNumberOfMetrics
) {
setDisabledNext(true);
} else {
setDisabledNext(false);
}
if (update === true) {
CallbackToSetVars(dashboardDetails);
setUpdate(false);
}
}, [update]);
useEffect(() => {
if (dashboardDetails.selectedPanelGroupMap?.length === 0) {
generateAlert();
}
}, [dashboardDetails.selectedPanelGroupMap]);
return (
<div className={classes.root}>
{selectedDashboard.dashboardJSON &&
selectedDashboard.dashboardJSON.panelGroupMap ? (
selectedDashboard.dashboardJSON.panelGroupMap.map(
(panelGroup: PanelGroupMap, index: number) => (
<div
key={`${panelGroup.groupName}-applicationDashboard-form`}
className={classes.panelGroupMap}
>
<Typography
align="left"
display="inline"
className={classes.panelGroupName}
>
{panelGroup.groupName}
</Typography>
<FormGroup
key={`metrics-group-${panelGroup.groupName}`}
className={classes.formGroup}
>
{panelGroup.panels.map((panel: string) => (
<FormControlLabel
control={
<CheckBox
checked={
dashboardDetails.selectedPanelGroupMap
? dashboardDetails.selectedPanelGroupMap[
index
].panels.includes(panel)
: false
}
onChange={() => handleMetricSelect(panel, index)}
name={panel}
/>
}
label={
<Typography className={classes.formControlLabel}>
{panel}
</Typography>
}
key={`metrics-group-${panelGroup.groupName}-label`}
/>
))}
</FormGroup>
</div>
)
)
) : (
<Typography
align="left"
display="inline"
className={classes.panelGroupName}
>
{t(
'analyticsDashboard.applicationDashboards.selectTheMetrics.errorMessage'
)}
</Typography>
)}
</div>
);
};
export default SelectTheMetricsForm;

View File

@ -0,0 +1,33 @@
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles((theme) => ({
root: {
background: theme.palette.background.paper,
height: '100%',
width: '100%',
border: 1,
borderRadius: '3px',
padding: theme.spacing(0, 4),
},
formControlLabel: {
fontSize: '1rem',
lineHeight: '140%',
},
panelGroupMap: {
margin: theme.spacing(2.5, 0, 2.5, 1),
},
panelGroupName: {
fontWeight: 500,
fontSize: '1.125rem',
lineHeight: '1.375rem',
letterSpacing: '0.1142px',
color: theme.palette.text.hint,
},
formGroup: {
flexDirection: 'row',
gap: '1rem',
marginTop: theme.spacing(1),
},
}));
export default useStyles;

View File

@ -0,0 +1,64 @@
import { Typography } from '@material-ui/core';
import React, { forwardRef, useImperativeHandle } from 'react';
import { useTranslation } from 'react-i18next';
import { DashboardDetails } from '../../../../../models/dashboardsData';
import useActions from '../../../../../redux/actions';
import * as AlertActions from '../../../../../redux/actions/alert';
import SelectTheMetricsForm from './Form';
import useStyles from './styles';
interface SelectTheMetricsProps {
dashboardVars: DashboardDetails;
handleMetricsUpdate: (dashboardMetrics: DashboardDetails) => void;
setDisabledNext: (next: boolean) => void;
}
const SelectTheMetrics = forwardRef(
(
{
dashboardVars,
handleMetricsUpdate,
setDisabledNext,
}: SelectTheMetricsProps,
ref
) => {
const classes = useStyles();
const { t } = useTranslation();
const alert = useActions(AlertActions);
function onNext() {
return true;
}
useImperativeHandle(ref, () => ({
onNext,
}));
return (
<div>
<Typography className={classes.heading}>
{t(
'analyticsDashboard.applicationDashboards.selectTheMetrics.header'
)}
</Typography>
<Typography className={classes.description}>
{t(
'analyticsDashboard.applicationDashboards.selectTheMetrics.description'
)}
</Typography>
<div className={classes.metricsForm}>
<SelectTheMetricsForm
dashboardVars={dashboardVars}
CallbackToSetVars={handleMetricsUpdate}
setDisabledNext={setDisabledNext}
generateAlert={() => {
alert.changeAlertState(true);
}}
/>
</div>
</div>
);
}
);
export default SelectTheMetrics;

View File

@ -0,0 +1,26 @@
import { makeStyles, Theme } from '@material-ui/core/styles';
const useStyles = makeStyles((theme: Theme) => ({
heading: {
fontWeight: 500,
fontSize: '1.5rem',
lineHeight: '1.8125rem',
padding: theme.spacing(0, 5),
},
description: {
width: '90%',
margin: theme.spacing(0.5, 0, 2),
fontSize: '1rem',
lineHeight: '150%',
letterSpacing: '0.1176px',
padding: theme.spacing(1, 5),
},
metricsForm: {
background: theme.palette.background.paper,
paddingBottom: theme.spacing(3),
},
}));
export default useStyles;

View File

@ -0,0 +1,87 @@
import { useLazyQuery } from '@apollo/client';
import { useTheme } from '@material-ui/core';
import { LineAreaGraph } from 'litmus-ui';
import React, { useEffect, useState } from 'react';
import { PROM_QUERY } from '../../../../../../../../graphql';
import {
PanelDetails,
ParsedPrometheusData,
} from '../../../../../../../../models/dashboardsData';
import {
PrometheusQueryVars,
PrometheusResponse,
promInput,
} from '../../../../../../../../models/graphql/prometheus';
import { DataParserForPrometheus } from '../../../../../../../../utils/promUtils';
import useStyles from './styles';
interface GraphProps {
prometheusQueryData: promInput;
panelVars: PanelDetails;
}
const Graph: React.FC<GraphProps> = ({ prometheusQueryData, panelVars }) => {
const { palette } = useTheme();
const classes = useStyles();
const lineGraph: string[] = palette.graph.line;
const areaGraph: string[] = palette.graph.area;
const [firstLoad, setFirstLoad] = useState<boolean>(true);
const [graphData, setGraphData] = React.useState<ParsedPrometheusData>({
seriesData: [],
closedAreaData: [],
chaosData: [],
});
const [getGraphData] = useLazyQuery<PrometheusResponse, PrometheusQueryVars>(
PROM_QUERY,
{
variables: {
prometheusInput: prometheusQueryData,
},
fetchPolicy: 'no-cache',
onCompleted: (prometheusData) => {
if (prometheusData) {
const parsedData: ParsedPrometheusData = DataParserForPrometheus(
prometheusData,
lineGraph,
areaGraph,
panelVars.prom_queries
.filter((query) => query.close_area)
.map((query) => query.queryid)
);
setGraphData(parsedData);
}
},
}
);
useEffect(() => {
if (
firstLoad &&
prometheusQueryData?.ds_details.url !== '' &&
prometheusQueryData?.queries?.length !== 0
) {
getGraphData();
setFirstLoad(false);
}
}, [firstLoad]);
return (
<div className={classes.graph}>
<LineAreaGraph
legendTableHeight={120}
openSeries={graphData.seriesData}
closedSeries={graphData.closedAreaData}
showGrid={panelVars.panel_options.grids}
showPoints={panelVars.panel_options.points}
showLegendTable
showTips={false}
unit={panelVars.unit}
yLabel={panelVars.y_axis_left}
yLabelOffset={55}
margin={{ left: 80, right: 20, top: 20, bottom: 10 }}
/>
</div>
);
};
export default Graph;

View File

@ -0,0 +1,12 @@
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles((theme) => ({
graph: {
backgroundColor: theme.palette.background.paper,
position: 'relative',
width: '98.5%',
height: '27.5rem',
},
}));
export default useStyles;

View File

@ -0,0 +1,153 @@
/* eslint-disable no-param-reassign */
import { Typography } from '@material-ui/core';
import React, { useEffect, useState } from 'react';
import AceEditor from 'react-ace';
import 'ace-builds/src-min-noconflict/ext-beautify';
import 'ace-builds/src-min-noconflict/ext-code_lens';
import 'ace-builds/src-min-noconflict/ext-elastic_tabstops_lite';
import 'ace-builds/src-min-noconflict/ext-emmet';
import 'ace-builds/src-min-noconflict/ext-error_marker';
import 'ace-builds/src-min-noconflict/ext-keybinding_menu';
import 'ace-builds/src-min-noconflict/ext-language_tools';
import 'ace-builds/src-min-noconflict/ext-linking';
import 'ace-builds/src-min-noconflict/ext-modelist';
import 'ace-builds/src-min-noconflict/ext-options';
import 'ace-builds/src-min-noconflict/ext-prompt';
import 'ace-builds/src-min-noconflict/ext-rtl';
import 'ace-builds/src-min-noconflict/ext-searchbox';
import 'ace-builds/src-min-noconflict/ext-spellcheck';
import 'ace-builds/src-min-noconflict/ext-split';
import 'ace-builds/src-min-noconflict/ext-static_highlight';
import 'ace-builds/src-min-noconflict/ext-statusbar';
import 'ace-builds/src-min-noconflict/ext-textarea';
import 'ace-builds/src-min-noconflict/ext-themelist';
import 'ace-builds/src-min-noconflict/ext-whitespace';
import { useTranslation } from 'react-i18next';
import './prometheus';
import useStyles from './styles';
import './theme';
interface PrometheusQueryEditorProps {
index: number;
content: string;
saveQueryChange: (updatedQuery: string) => void;
seriesListCompletionOptions: any;
labelListCompletionOptions: any;
valueListCompletionOptions: any;
}
const PrometheusQueryEditor: React.FC<PrometheusQueryEditorProps> = ({
index,
content,
saveQueryChange,
seriesListCompletionOptions,
labelListCompletionOptions,
valueListCompletionOptions,
}) => {
const classes = useStyles();
const { t } = useTranslation();
const [modifiedPrometheusQuery, setModifiedPrometheusQuery] =
useState(content);
const [autoCompleteOptions, setAutoCompleteOptions] = useState([
...seriesListCompletionOptions,
...labelListCompletionOptions,
...valueListCompletionOptions,
]);
const PromAce = React.createRef() as React.RefObject<AceEditor>;
useEffect(() => {
setModifiedPrometheusQuery(content);
}, [content]);
useEffect(() => {
setAutoCompleteOptions([
...seriesListCompletionOptions,
...labelListCompletionOptions,
...valueListCompletionOptions,
]);
}, [
seriesListCompletionOptions,
labelListCompletionOptions,
valueListCompletionOptions,
]);
return (
<div id={`editor-${index}`} data-cy="WorkflowEditor">
<div className={classes.editor}>
<Typography className={classes.heading}>
{t('analyticsDashboard.applicationDashboards.tuneTheQueries.query')}
</Typography>
<pre id="prom-query-editor">
<AceEditor
mode="prometheus"
theme="prom-query-editor"
width="100%"
height="100%"
maxLines={12000}
minLines={1}
highlightActiveLine={false}
readOnly={false}
tabSize={2}
wrapEnabled
ref={PromAce}
showGutter={false}
onChange={(value: string) => {
setModifiedPrometheusQuery(value);
saveQueryChange(value);
}}
showPrintMargin={false}
enableSnippets={false}
value={modifiedPrometheusQuery}
editorProps={{
$blockScrolling: Infinity,
$useWorker: true,
}}
onLoad={(editor) => {
editor.setReadOnly(false);
editor.setOptions({
fontFamily: 'monospace',
highlightGutterLine: false,
autoScrollEditorIntoView: true,
tooltipFollowsMouse: true,
displayIndentGuides: false,
enableBasicAutocompletion: [
{
getCompletions: (
_editor: any,
_session: any,
_pos: any,
_prefix: any,
callback: any
) => {
// note, won't fire if caret is at a word that does not have these letters
callback(null, autoCompleteOptions);
},
},
],
// to make popup appear automatically, without explicit _ctrl+space_
enableLiveAutocompletion: true,
});
editor.setHighlightSelectedWord(true);
editor.session.setFoldStyle('markbeginend');
editor.setShowFoldWidgets(false);
editor.setAnimatedScroll(true);
editor.setShowInvisibles(false);
editor.setFontSize('0.98rem');
editor.container.style.lineHeight = '160%';
}}
onCursorChange={() => {
if (PromAce.current) {
(PromAce.current!.editor as any).setOptions({
autoScrollEditorIntoView: true,
tooltipFollowsMouse: true,
});
}
}}
/>
</pre>
</div>
</div>
);
};
export default PrometheusQueryEditor;

View File

@ -0,0 +1,597 @@
/* eslint-disable no-param-reassign */
import ace from 'ace-builds';
(ace as any)['define'](
'ace/mode/prometheus',
[
'require',
'exports',
'module',
'ace/lib/oop',
'ace/mode/text',
'ace/mode/text_highlight_rules',
'ace/token_iterator',
'ace/lib/lang',
],
function CustomMode(this: any, acequire: any, exports: any) {
const oop = acequire('../lib/oop');
const TextMode = acequire('./text').Mode;
const { TextHighlightRules } = acequire('./text_highlight_rules');
const PrometheusHighlightRules = function HighlightRules(this: any) {
const keywords =
'count|count_values|min|max|avg|sum|stddev|stdvar|bottomk|topk|quantile';
const builtinConstants = 'true|false|null|__name__|job';
const builtinFunctions =
'abs|absent|ceil|changes|clamp_max|clamp_min|count_scalar|day_of_month|day_of_week|days_in_month|delta|deriv|' +
'drop_common_labels|exp|floor|histogram_quantile|holt_winters|hour|idelta|increase|irate|label_replace|ln|log2|' +
'log10|minute|month|predict_linear|rate|resets|round|scalar|sort|sort_desc|sqrt|time|vector|year|avg_over_time|' +
'min_over_time|max_over_time|sum_over_time|count_over_time|quantile_over_time|stddev_over_time|stdvar_over_time';
const keywordMapper = this.createKeywordMapper(
{
'support.function': builtinFunctions,
keyword: keywords,
'constant.language': builtinConstants,
},
'identifier',
true
);
this.$rules = {
start: [
{
token: 'string', // single line
regex: /"(?:[^"\\]|\\.)*?"/,
},
{
token: 'string', // string
regex: "'.*?'",
},
{
token: 'constant.numeric', // float
regex: '[-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b',
},
{
token: 'constant.language', // time
regex: '\\d+[smhdwy]',
},
{
token: 'keyword.operator.binary',
regex: '\\+|\\-|\\*|\\/|%|\\^|==|!=|<=|>=|<|>|and|or|unless',
},
{
token: 'keyword.other',
regex: 'keep_common|offset|bool',
},
{
token: 'keyword.control',
regex: 'by|without|on|ignoring|group_left|group_right',
next: 'start-label-list-matcher',
},
{
token: 'variable',
regex: '\\$[A-Za-z0-9_]+',
},
{
token: keywordMapper,
regex: '[a-zA-Z_:][a-zA-Z0-9_:]*',
},
{
token: 'paren.lparen',
regex: '[[(]',
},
{
token: 'paren.lparen.label-matcher',
regex: '{',
next: 'start-label-matcher',
},
{
token: 'paren.rparen',
regex: '[\\])]',
},
{
token: 'paren.rparen.label-matcher',
regex: '}',
},
{
token: 'text',
regex: '\\s+',
},
],
'start-label-matcher': [
{
token: 'entity.name.tag.label-matcher',
regex: '[a-zA-Z_][a-zA-Z0-9_]*',
},
{
token: 'keyword.operator.label-matcher',
regex: '=~|=|!~|!=',
},
{
token: 'string.quoted.label-matcher',
regex: '"[^"]*"|\'[^\']*\'',
},
{
token: 'punctuation.operator.label-matcher',
regex: ',',
},
{
token: 'paren.rparen.label-matcher',
regex: '}',
next: 'start',
},
],
'start-label-list-matcher': [
{
token: 'paren.lparen.label-list-matcher',
regex: '[(]',
},
{
token: 'entity.name.tag.label-list-matcher',
regex: '[a-zA-Z_][a-zA-Z0-9_]*',
},
{
token: 'punctuation.operator.label-list-matcher',
regex: ',',
},
{
token: 'paren.rparen.label-list-matcher',
regex: '[)]',
next: 'start',
},
],
};
this.normalizeRules();
};
oop.inherits(PrometheusHighlightRules, TextHighlightRules);
const lang = acequire('../lib/lang');
const prometheusKeyWords = [
'by',
'without',
'keep_common',
'offset',
'bool',
'and',
'or',
'unless',
'ignoring',
'on',
'group_left',
'group_right',
'count',
'count_values',
'min',
'max',
'avg',
'sum',
'stddev',
'stdvar',
'bottomk',
'topk',
'quantile',
];
const keyWordsCompletions = prometheusKeyWords.map((word: any) => {
return {
caption: word,
value: word,
meta: 'keyword',
score: Number.MAX_VALUE,
};
});
const prometheusFunctions = [
{
name: 'abs()',
value: 'abs',
def: 'abs(v instant-vector)',
docText:
'Returns the input vector with all sample values converted to their absolute value.',
},
{
name: 'absent()',
value: 'absent',
def: 'absent(v instant-vector)',
docText:
'Returns an empty vector if the vector passed to it has any elements and a 1-element vector with the value 1 if the vector passed to it has no elements. This is useful for alerting on when no time series exist for a given metric name and label combination.',
},
{
name: 'ceil()',
value: 'ceil',
def: 'ceil(v instant-vector)',
docText:
'Rounds the sample values of all elements in `v` up to the nearest integer.',
},
{
name: 'changes()',
value: 'changes',
def: 'changes(v range-vector)',
docText:
'For each input time series, `changes(v range-vector)` returns the number of times its value has changed within the provided time range as an instant vector.',
},
{
name: 'clamp_max()',
value: 'clamp_max',
def: 'clamp_max(v instant-vector, max scalar)',
docText:
'Clamps the sample values of all elements in `v` to have an upper limit of `max`.',
},
{
name: 'clamp_min()',
value: 'clamp_min',
def: 'clamp_min(v instant-vector, min scalar)',
docText:
'Clamps the sample values of all elements in `v` to have a lower limit of `min`.',
},
{
name: 'count_scalar()',
value: 'count_scalar',
def: 'count_scalar(v instant-vector)',
docText:
'Returns the number of elements in a time series vector as a scalar. This is in contrast to the `count()` aggregation operator, which always returns a vector (an empty one if the input vector is empty) and allows grouping by labels via a `by` clause.',
},
{
name: 'day_of_month()',
value: 'day_of_month',
def: 'day_of_month(v=vector(time()) instant-vector)',
docText:
'Returns the day of the month for each of the given times in UTC. Returned values are from 1 to 31.',
},
{
name: 'day_of_week()',
value: 'day_of_week',
def: 'day_of_week(v=vector(time()) instant-vector)',
docText:
'Returns the day of the week for each of the given times in UTC. Returned values are from 0 to 6, where 0 means Sunday etc.',
},
{
name: 'days_in_month()',
value: 'days_in_month',
def: 'days_in_month(v=vector(time()) instant-vector)',
docText:
'Returns number of days in the month for each of the given times in UTC. Returned values are from 28 to 31.',
},
{
name: 'delta()',
value: 'delta',
def: 'delta(v range-vector)',
docText:
'Calculates the difference between the first and last value of each time series element in a range vector `v`, returning an instant vector with the given deltas and equivalent labels. The delta is extrapolated to cover the full time range as specified in the range vector selector, so that it is possible to get a non-integer result even if the sample values are all integers.',
},
{
name: 'deriv()',
value: 'deriv',
def: 'deriv(v range-vector)',
docText:
'Calculates the per-second derivative of the time series in a range vector `v`, using simple linear regression.',
},
{
name: 'drop_common_labels()',
value: 'drop_common_labels',
def: 'drop_common_labels(instant-vector)',
docText:
'Drops all labels that have the same name and value across all series in the input vector.',
},
{
name: 'exp()',
value: 'exp',
def: 'exp(v instant-vector)',
docText:
'Calculates the exponential function for all elements in `v`.\nSpecial cases are:\n* `Exp(+Inf) = +Inf` \n* `Exp(NaN) = NaN`',
},
{
name: 'floor()',
value: 'floor',
def: 'floor(v instant-vector)',
docText:
'Rounds the sample values of all elements in `v` down to the nearest integer.',
},
{
name: 'histogram_quantile()',
value: 'histogram_quantile',
def: 'histogram_quantile(φ float, b instant-vector)',
docText:
'Calculates the φ-quantile (0 ≤ φ ≤ 1) from the buckets `b` of a histogram. The samples in `b` are the counts of observations in each bucket. Each sample must have a label `le` where the label value denotes the inclusive upper bound of the bucket. (Samples without such a label are silently ignored.) The histogram metric type automatically provides time series with the `_bucket` suffix and the appropriate labels.',
},
{
name: 'holt_winters()',
value: 'holt_winters',
def: 'holt_winters(v range-vector, sf scalar, tf scalar)',
docText:
'Produces a smoothed value for time series based on the range in `v`. The lower the smoothing factor `sf`, the more importance is given to old data. The higher the trend factor `tf`, the more trends in the data is considered. Both `sf` and `tf` must be between 0 and 1.',
},
{
name: 'hour()',
value: 'hour',
def: 'hour(v=vector(time()) instant-vector)',
docText:
'Returns the hour of the day for each of the given times in UTC. Returned values are from 0 to 23.',
},
{
name: 'idelta()',
value: 'idelta',
def: 'idelta(v range-vector)',
docText:
'Calculates the difference between the last two samples in the range vector `v`, returning an instant vector with the given deltas and equivalent labels.',
},
{
name: 'increase()',
value: 'increase',
def: 'increase(v range-vector)',
docText:
'Calculates the increase in the time series in the range vector. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for. The increase is extrapolated to cover the full time range as specified in the range vector selector, so that it is possible to get a non-integer result even if a counter increases only by integer increments.',
},
{
name: 'irate()',
value: 'irate',
def: 'irate(v range-vector)',
docText:
'Calculates the per-second instant rate of increase of the time series in the range vector. This is based on the last two data points. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for.',
},
{
name: 'label_replace()',
value: 'label_replace',
def: 'label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)',
docText:
"For each timeseries in `v`, `label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)` matches the regular expression `regex` against the label `src_label`. If it matches, then the timeseries is returned with the label `dst_label` replaced by the expansion of `replacement`. `$1` is replaced with the first matching subgroup, `$2` with the second etc. If the regular expression doesn't match then the timeseries is returned unchanged.",
},
{
name: 'ln()',
value: 'ln',
def: 'ln(v instant-vector)',
docText:
'calculates the natural logarithm for all elements in `v`.\nSpecial cases are:\n * `ln(+Inf) = +Inf`\n * `ln(0) = -Inf`\n * `ln(x < 0) = NaN`\n * `ln(NaN) = NaN`',
},
{
name: 'log2()',
value: 'log2',
def: 'log2(v instant-vector)',
docText:
'Calculates the binary logarithm for all elements in `v`. The special cases are equivalent to those in `ln`.',
},
{
name: 'log10()',
value: 'log10',
def: 'log10(v instant-vector)',
docText:
'Calculates the decimal logarithm for all elements in `v`. The special cases are equivalent to those in `ln`.',
},
{
name: 'minute()',
value: 'minute',
def: 'minute(v=vector(time()) instant-vector)',
docText:
'Returns the minute of the hour for each of the given times in UTC. Returned values are from 0 to 59.',
},
{
name: 'month()',
value: 'month',
def: 'month(v=vector(time()) instant-vector)',
docText:
'Returns the month of the year for each of the given times in UTC. Returned values are from 1 to 12, where 1 means January etc.',
},
{
name: 'predict_linear()',
value: 'predict_linear',
def: 'predict_linear(v range-vector, t scalar)',
docText:
'Predicts the value of time series `t` seconds from now, based on the range vector `v`, using simple linear regression.',
},
{
name: 'rate()',
value: 'rate',
def: 'rate(v range-vector)',
docText:
"Calculates the per-second average rate of increase of the time series in the range vector. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for. Also, the calculation extrapolates to the ends of the time range, allowing for missed scrapes or imperfect alignment of scrape cycles with the range's time period.",
},
{
name: 'resets()',
value: 'resets',
def: 'resets(v range-vector)',
docText:
'For each input time series, `resets(v range-vector)` returns the number of counter resets within the provided time range as an instant vector. Any decrease in the value between two consecutive samples is interpreted as a counter reset.',
},
{
name: 'round()',
value: 'round',
def: 'round(v instant-vector, to_nearest=1 scalar)',
docText:
'Rounds the sample values of all elements in `v` to the nearest integer. Ties are resolved by rounding up. The optional `to_nearest` argument allows specifying the nearest multiple to which the sample values should be rounded. This multiple may also be a fraction.',
},
{
name: 'scalar()',
value: 'scalar',
def: 'scalar(v instant-vector)',
docText:
'Given a single-element input vector, `scalar(v instant-vector)` returns the sample value of that single element as a scalar. If the input vector does not have exactly one element, `scalar` will return `NaN`.',
},
{
name: 'sort()',
value: 'sort',
def: 'sort(v instant-vector)',
docText:
'Returns vector elements sorted by their sample values, in ascending order.',
},
{
name: 'sort_desc()',
value: 'sort_desc',
def: 'sort_desc(v instant-vector)',
docText:
'Returns vector elements sorted by their sample values, in descending order.',
},
{
name: 'sqrt()',
value: 'sqrt',
def: 'sqrt(v instant-vector)',
docText: 'Calculates the square root of all elements in `v`.',
},
{
name: 'time()',
value: 'time',
def: 'time()',
docText:
'Returns the number of seconds since January 1, 1970 UTC. Note that this does not actually return the current time, but the time at which the expression is to be evaluated.',
},
{
name: 'vector()',
value: 'vector',
def: 'vector(s scalar)',
docText: 'Returns the scalar `s` as a vector with no labels.',
},
{
name: 'year()',
value: 'year',
def: 'year(v=vector(time()) instant-vector)',
docText: 'Returns the year for each of the given times in UTC.',
},
{
name: 'avg_over_time()',
value: 'avg_over_time',
def: 'avg_over_time(range-vector)',
docText: 'The average value of all points in the specified interval.',
},
{
name: 'min_over_time()',
value: 'min_over_time',
def: 'min_over_time(range-vector)',
docText: 'The minimum value of all points in the specified interval.',
},
{
name: 'max_over_time()',
value: 'max_over_time',
def: 'max_over_time(range-vector)',
docText: 'The maximum value of all points in the specified interval.',
},
{
name: 'sum_over_time()',
value: 'sum_over_time',
def: 'sum_over_time(range-vector)',
docText: 'The sum of all values in the specified interval.',
},
{
name: 'count_over_time()',
value: 'count_over_time',
def: 'count_over_time(range-vector)',
docText: 'The count of all values in the specified interval.',
},
{
name: 'quantile_over_time()',
value: 'quantile_over_time',
def: 'quantile_over_time(scalar, range-vector)',
docText:
'The φ-quantile (0 ≤ φ ≤ 1) of the values in the specified interval.',
},
{
name: 'stddev_over_time()',
value: 'stddev_over_time',
def: 'stddev_over_time(range-vector)',
docText:
'The population standard deviation of the values in the specified interval.',
},
{
name: 'stdvar_over_time()',
value: 'stdvar_over_time',
def: 'stdvar_over_time(range-vector)',
docText:
'The population standard variance of the values in the specified interval.',
},
];
const wrapText = (str: any, len: any) => {
const length = len || 60;
const lines = [];
let space_index = 0;
let line_start = 0;
let next_line_end = length;
let line = '';
for (let i = 0; i < str.length; i++) {
if (str[i] === ' ') {
space_index = i;
} else if (i >= next_line_end && space_index !== 0) {
line = str.slice(line_start, space_index);
lines.push(line);
line_start = space_index + 1;
next_line_end = i + length;
space_index = 0;
}
}
line = str.slice(line_start);
lines.push(line);
return lines.join('&nbsp<br>');
};
const convertMarkDownTags = (text: any) => {
let newText = text.replace(/```(.+)```/, '<pre>$1</pre>');
newText = newText.replace(/`([^`]+)`/, '<code>$1</code>');
return newText;
};
const convertToHTML = (item: any) => {
let docText = lang.escapeHTML(item.docText);
docText = convertMarkDownTags(wrapText(docText, 40));
return [
'<b>',
lang.escapeHTML(item.def),
'</b>',
'<hr></hr>',
docText,
'<br>&nbsp',
].join('');
};
const functionsCompletions = prometheusFunctions.map((item: any) => {
return {
caption: item.name,
value: item.value,
docHTML: convertToHTML(item),
meta: 'function',
score: Number.MAX_VALUE,
};
});
const PrometheusCompletions = () => {};
(function CompletionContainer(this: any) {
this.getCompletions = (
state: any,
session: any,
pos: any,
prefix: any,
callback: any
) => {
const token = session.getTokenAt(pos.row, pos.column);
if (
token.type === 'entity.name.tag.label-matcher' ||
token.type === 'string.quoted.label-matcher' ||
token.type === 'entity.name.tag.label-list-matcher'
) {
return callback(null, []);
}
const completions = keyWordsCompletions.concat(functionsCompletions);
return callback(null, completions);
};
}.call(PrometheusCompletions.prototype));
const Mode = function Generator(this: any) {
this.HighlightRules = PrometheusHighlightRules;
this.$completer = PrometheusCompletions;
// replace keyWordCompleter
this.completer = this.$completer;
};
oop.inherits(Mode, TextMode);
(function ModeBinder(this: any) {
this.$id = 'ace/mode/prometheus';
}.call(Mode.prototype));
exports.Mode = Mode;
}
);

View File

@ -0,0 +1,44 @@
import { makeStyles, Theme } from '@material-ui/core';
const useStyles = makeStyles((theme: Theme) => ({
// Editor
editor: {
width: '100%',
height: 'fit-content',
paddingTop: theme.spacing(1.5),
'&::-webkit-scrollbar': {
width: '0.2em',
},
'&::-webkit-scrollbar-track': {
webkitBoxShadow: `inset 0 0 6px ${theme.palette.common.black}`,
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: theme.palette.primary.main,
},
'& #prom-query-editor': {
overflowY: 'auto',
width: '100%',
height: '100%',
position: 'relative',
border: `1.5px solid ${theme.palette.border.main}`,
borderRadius: '0.25rem',
margin: 0,
},
},
heading: {
color: theme.palette.text.hint,
fontSize: '0.7rem',
lineHeight: '160%',
zIndex: 2,
position: 'relative',
background: theme.palette.background.paper,
width: 'fit-content',
margin: theme.spacing(0, 0, -1, 1.25),
padding: theme.spacing(0, 0.5),
},
}));
export default useStyles;

View File

@ -0,0 +1,121 @@
/* eslint-disable no-param-reassign */
/* eslint-disable no-multi-str */
import ace from 'ace-builds';
(ace as any)['define'](
'ace/theme/prom-query-editor',
['require', 'exports', 'module', 'ace/lib/dom'],
(acequire: any, exports: any) => {
exports.isDark = false;
exports.cssClass = 'lp-code-bright';
exports.cssText =
'.lp-code-bright .ace_gutter {\
background: #2f3129;\
color: #8f908a\
}\
.lp-code-bright .ace_print-margin {\
width: 1px;\
background: #555651\
}\
.lp-code-bright {\
background-color: #FFF;\
color: #1C0732\
}\
.lp-code-bright .ace_cursor {\
color: #1C0732\
}\
.lp-code-bright .ace_marker-layer .ace_selection {\
background: rgba(31, 0, 230, 0.15)\
}\
.lp-code-bright.ace_multiselect .ace_selection.ace_start {\
box-shadow: 0 0 3px 0px #272822;\
}\
.lp-code-bright .ace_marker-layer .ace_step {\
background: rgb(102, 82, 0)\
}\
.lp-code-bright .ace_marker-layer .ace_bracket {\
margin: -1px 0 0 -1px;\
border: 1px solid rgba(31, 0, 230, 0.15)\
}\
.lp-code-bright .ace_marker-layer .ace_active-line {\
background: #202020\
}\
.lp-code-bright .ace_gutter-active-line {\
background-color: #272727\
}\
.lp-code-bright .ace_marker-layer .ace_selected-word {\
border: 1px solid rgba(31, 0, 230, 0.15)\
}\
.lp-code-bright .ace_invisible {\
color: #52524d\
}\
.lp-code-bright .ace_keyword{\
color: #f92672\
}\
.lp-code-bright .ace_entity.ace_name.ace_tag,\
.lp-code-bright .ace_meta.ace_tag,\
.lp-code-bright .ace_storage {\
color: #0070EE\
}\
.lp-code-bright .ace_punctuation,\
.lp-code-bright .ace_punctuation.ace_tag {\
color: #0070EE\
}\
.lp-code-bright .ace_constant.ace_character,\
.lp-code-bright .ace_constant.ace_language,\
.lp-code-bright .ace_constant.ace_numeric,\
.lp-code-bright .ace_constant.ace_other {\
color: #fe85fc\
}\
.lp-code-bright .ace_invalid {\
color: #f8f8f0;\
background-color: #f92672\
}\
.lp-code-bright .ace_invalid.ace_deprecated {\
color: #f8f8f0;\
background-color: #ae81ff\
}\
.lp-code-bright .ace_support.ace_constant,\
.lp-code-bright .ace_support.ace_function {\
color: #006400\
}\
.lp-code-bright .ace_fold {\
background-color: #a6e22e;\
border-color: #f8f8f2\
}\
.lp-code-bright .ace_storage.ace_type,\
.lp-code-bright .ace_support.ace_class,\
.lp-code-bright .ace_support.ace_type {\
font-style: italic;\
color: #74e680\
}\
.lp-code-bright .ace_entity.ace_name.ace_function,\
.lp-code-bright .ace_entity.ace_other,\
.lp-code-bright .ace_entity.ace_other.ace_attribute-name,\
.lp-code-bright .ace_variable {\
color: #0070EE\
}\
.lp-code-bright .ace_variable.ace_parameter {\
font-style: italic;\
color: #f0a842\
}\
.lp-code-bright .ace_string {\
color: #5B44BA\
}\
.lp-code-bright .ace_paren {\
color: #ED5C0C\
}\
.lp-code-bright .ace_operator {\
color: #59e6e3\
}\
.lp-code-bright .ace_comment {\
color: #75715e\
}\
.lp-code-bright .ace_indent-guide {\
background: url(data:image/png;base64,ivborw0kggoaaaansuheugaaaaeaaaaccayaaaczgbynaaaaekleqvqimwpq0fd0zxbzd/wpaajvaoxesgneaaaaaelftksuqmcc) right repeat-y\
}';
const dom = acequire('ace/lib/dom');
dom.importCssString(exports.cssText, exports.cssClass);
}
);

View File

@ -0,0 +1,676 @@
/* eslint-disable no-unused-expressions */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useLazyQuery } from '@apollo/client';
import {
FormControl,
IconButton,
InputLabel,
MenuItem,
Select,
TextField,
Typography,
} from '@material-ui/core';
import AccordionDetails from '@material-ui/core/AccordionDetails';
import AccordionSummary from '@material-ui/core/AccordionSummary';
import { Autocomplete } from '@material-ui/lab';
import { AutocompleteChipInput, InputField } from 'litmus-ui';
import moment from 'moment';
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Accordion } from '../../../../../../../../components/Accordion';
import InfoTooltip from '../../../../../../../../components/InfoTooltip';
import { PROM_LABEL_VALUES } from '../../../../../../../../graphql';
import {
PromQueryDetails,
QueryLabelValue,
} from '../../../../../../../../models/dashboardsData';
import { ApplicationMetadata } from '../../../../../../../../models/graphql/dashboardsDetails';
import {
LabelValue,
PrometheusSeriesQueryVars,
PrometheusSeriesResponse,
} from '../../../../../../../../models/graphql/prometheus';
import { ReactComponent as ExpandAccordion } from '../../../../../../../../svg/expandQueryAccordion.svg';
import { ReactComponent as CopyQuery } from '../../../../../../../../svg/queryCopy.svg';
import { ReactComponent as DeleteQuery } from '../../../../../../../../svg/queryDelete.svg';
import { ReactComponent as ShowHideQuery } from '../../../../../../../../svg/queryHide.svg';
import { ReactComponent as ShrinkAccordion } from '../../../../../../../../svg/shrinkQueryAccordion.svg';
import {
getLabelsAndValues,
setLabelsAndValues,
} from '../../../../../../../../utils/promUtils';
import { validateTimeInSeconds } from '../../../../../../../../utils/validate';
import PrometheusQueryEditor from './PrometheusQueryBox';
import useStyles from './styles';
interface QueryEditorProps {
index: number;
promQuery: PromQueryDetails;
selectedApps: ApplicationMetadata[];
dsURL: string;
seriesList: Option[];
handleDeleteQuery: (index: number) => void;
handleShowAndHideQuery: (index: number) => void;
handleUpdateQuery: (query: PromQueryDetails, index: number) => void;
}
interface Option {
name: string;
[index: string]: any;
}
const resolutions: string[] = ['1/1', '1/2', '1/3', '1/4', '1/5', '1/10'];
const QueryEditor: React.FC<QueryEditorProps> = ({
index,
promQuery,
selectedApps,
dsURL,
seriesList,
handleDeleteQuery,
handleShowAndHideQuery,
handleUpdateQuery,
}) => {
const classes = useStyles();
const { t } = useTranslation();
const [open, setOpen] = React.useState<boolean>(true);
const [selectedValuesForLabel, setSelectedValuesForLabel] = React.useState<
Array<Option>
>([]);
const [selectedLabel, setSelectedLabel] = React.useState<string>('');
const [update, setUpdate] = React.useState<boolean>(false);
const [firstLoad, setFirstLoad] = React.useState<boolean>(true);
const [localQuery, setLocalQuery] = React.useState<PromQueryDetails>({
...promQuery,
base_query: promQuery.prom_query_name.split('{')[0].includes('(')
? promQuery.prom_query_name
.split('{')[0]
.substring(
promQuery.prom_query_name.split('{')[0].lastIndexOf('(') + 1
)
: promQuery.prom_query_name.split('{')[0],
labels_and_values_list: getLabelsAndValues(promQuery.prom_query_name),
});
const [getLabelValues, { data: labelValueData }] = useLazyQuery<
PrometheusSeriesResponse,
PrometheusSeriesQueryVars
>(PROM_LABEL_VALUES, {
variables: {
prometheusInput: {
ds_details: {
url: dsURL,
start: `${
new Date(
moment
.unix(Math.round(new Date().getTime() / 1000) - 900)
.format()
).getTime() / 1000
}`,
end: `${
new Date(
moment.unix(Math.round(new Date().getTime() / 1000)).format()
).getTime() / 1000
}`,
},
series: localQuery.base_query ?? '',
},
},
fetchPolicy: 'network-only',
});
const getAvailableValues = (label: string) => {
let options: Array<Option> = [];
if (labelValueData) {
labelValueData.GetPromLabelNamesAndValues.labelValues?.forEach(
(labelValue) => {
if (labelValue.label === label) {
options = labelValue.values ?? [];
}
}
);
}
return options;
};
const getSelectedValuesForLabel = (label: string) => {
const allOptions: string[] = getAvailableValues(label).map(
(option) => option.name
);
const labelValuesList: QueryLabelValue[] = getLabelsAndValues(
localQuery.prom_query_name
);
const options: Array<Option> = [];
labelValuesList.forEach((labelValue) => {
if (labelValue.label === label) {
labelValue.value.forEach((item) => {
options.push({ name: item });
});
}
});
selectedApps.forEach((app) => {
app.applications.forEach((appRes) => {
if (
label !== 'job' &&
label.toLowerCase().includes(appRes.kind.toLowerCase())
) {
appRes.names.forEach((name) => {
if (allOptions.includes(name)) {
options.push({ name });
}
});
}
if (label === 'job') {
appRes.names.forEach((name) => {
if (allOptions.includes(name)) {
options.push({ name });
} else if (allOptions.includes(`${app.namespace}/${name}`)) {
options.push({ name: `${app.namespace}/${name}` });
}
});
}
});
});
setSelectedValuesForLabel(options);
};
useEffect(() => {
if (firstLoad && localQuery.base_query !== '' && dsURL !== '') {
getLabelValues();
getSelectedValuesForLabel(selectedLabel ?? '');
setFirstLoad(false);
}
}, [firstLoad]);
useEffect(() => {
if (update) {
handleUpdateQuery(localQuery, index);
setUpdate(false);
}
}, [update]);
const getValueList = (list: LabelValue[]) => {
const completionOptions: any[] = [];
list.forEach((labelValue) => {
labelValue.values?.forEach((value) => {
completionOptions.push({
value,
score: 3,
meta: `Value for ${labelValue.label}`,
});
});
});
return completionOptions;
};
const copyTextToClipboard = (text: string) => {
if (!navigator.clipboard) {
console.error('Oops Could not copy text: ');
return;
}
navigator.clipboard
.writeText(text)
.catch((err) => console.error('Async: Could not copy text: ', err));
};
return (
<div className={classes.root}>
<Accordion expanded={open}>
<AccordionSummary
expandIcon={
open ? (
<ShrinkAccordion
onClick={() => {
setOpen(false);
}}
/>
) : (
<ExpandAccordion
onClick={() => {
setOpen(true);
}}
/>
)
}
IconButtonProps={{ edge: 'start' }}
aria-controls={`query-${promQuery.queryid}-content`}
id={`query-${promQuery.queryid}-header`}
className={classes.query}
key={`${promQuery.queryid}`}
>
<div className={`${classes.flex} ${classes.summaryHeader}`}>
<Typography className={classes.queryTitle}>
{String.fromCharCode(65 + index)}
</Typography>
<div className={classes.flex}>
<IconButton
className={classes.iconButton}
onClick={() => {
copyTextToClipboard(localQuery.prom_query_name);
}}
>
<CopyQuery className={classes.icon} />
</IconButton>
<IconButton
className={classes.iconButton}
onClick={() => {
handleShowAndHideQuery(index);
}}
>
<ShowHideQuery className={classes.icon} />
</IconButton>
<IconButton
className={classes.iconButton}
onClick={() => {
handleDeleteQuery(index);
}}
>
<DeleteQuery className={classes.icon} />
</IconButton>
</div>
</div>
</AccordionSummary>
<AccordionDetails className={classes.queryContainer}>
<div style={{ width: '98.5%' }}>
<Autocomplete
value={{ name: localQuery.base_query ?? '' }}
freeSolo
id={`query-${promQuery.queryid}-query-name`}
options={seriesList}
getOptionLabel={(option: Option) => option.name}
style={{ width: '45%' }}
renderInput={(params) => (
<TextField
{...params}
label={t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.metric'
)}
variant="outlined"
size="medium"
InputLabelProps={{ className: classes.formLabel }}
/>
)}
onChange={(event, value, reason) => {
const newQuery: string = value
? reason === 'create-option'
? (value as string)
: (value as Option).name
: '';
setLocalQuery({
...localQuery,
base_query: newQuery,
prom_query_name: newQuery,
labels_and_values_list: [],
});
if (newQuery !== '' && dsURL !== '') {
setSelectedValuesForLabel([]);
getLabelValues();
}
setUpdate(true);
}}
/>
<div
className={`${classes.flex} ${classes.paddedTop}`}
style={{ gap: '1rem' }}
>
<FormControl
variant="outlined"
className={classes.formControl}
style={{ width: '25%' }}
color="primary"
>
<InputLabel className={classes.selectTextLabel}>
{t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.keys'
)}
</InputLabel>
<Select
value={selectedLabel}
onChange={(event: any) => {
setSelectedLabel(event.target.value as string);
getSelectedValuesForLabel(event.target.value as string);
}}
label={t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.selectKey'
)}
className={classes.selectText}
>
{labelValueData &&
labelValueData.GetPromLabelNamesAndValues.labelValues?.map(
(labelValue: LabelValue) => (
<MenuItem
key={labelValue.label}
value={labelValue.label}
>
{labelValue.label}
</MenuItem>
)
)}
</Select>
</FormControl>
<AutocompleteChipInput
value={selectedValuesForLabel}
onChange={(event, value, reason) => {
const selectedValues: Array<Option> = value as Array<Option>;
const existingLabelValuesList: QueryLabelValue[] =
localQuery.labels_and_values_list ?? [];
let updateStatus = false;
existingLabelValuesList.forEach((labelValue, index) => {
if (labelValue.label === selectedLabel) {
existingLabelValuesList[index].value = selectedValues.map(
(option) => option.name
);
updateStatus = true;
}
});
if (!updateStatus) {
existingLabelValuesList.push({
label: selectedLabel,
value: selectedValues.map((option) => option.name),
});
}
setLocalQuery({
...localQuery,
prom_query_name: setLabelsAndValues(
localQuery.base_query ?? '',
localQuery.prom_query_name ?? '',
existingLabelValuesList
),
labels_and_values_list: existingLabelValuesList,
});
getSelectedValuesForLabel(selectedLabel ?? '');
setUpdate(true);
}}
options={getAvailableValues(selectedLabel ?? '')}
label={t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.values'
)}
placeholder={`${t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.addValue'
)}`}
disableCloseOnSelect
disableClearable={false}
limitTags={4}
style={{ width: '75%' }}
/>
</div>
<PrometheusQueryEditor
index={index}
content={localQuery.prom_query_name ?? ''}
seriesListCompletionOptions={
seriesList.map((option: Option) => ({
value: option.name,
score: 1,
meta: t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.seriesName'
),
})) ?? []
}
labelListCompletionOptions={
labelValueData?.GetPromLabelNamesAndValues.labelValues?.map(
(labelValue: LabelValue) => ({
value: labelValue.label,
score: 2,
meta: localQuery.base_query
? `${t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.labelFor'
)} ${localQuery.base_query}`
: t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.label'
),
})
) ?? []
}
valueListCompletionOptions={getValueList(
labelValueData?.GetPromLabelNamesAndValues.labelValues ?? []
)}
saveQueryChange={(updatedQuery: string) => {
const existingBaseQuery: string = localQuery.base_query ?? '';
const newBaseQuery: string = updatedQuery
.split('{')[0]
.includes('(')
? updatedQuery
.split('{')[0]
.substring(
updatedQuery.split('{')[0].lastIndexOf('(') + 1
)
: updatedQuery.split('{')[0];
setLocalQuery({
...localQuery,
base_query: newBaseQuery,
labels_and_values_list: getLabelsAndValues(updatedQuery),
prom_query_name: updatedQuery,
});
if (
existingBaseQuery !== newBaseQuery &&
localQuery.base_query !== '' &&
dsURL !== ''
) {
getLabelValues();
setSelectedValuesForLabel([]);
}
setUpdate(true);
}}
/>
<Typography className={classes.configHeader}>
{t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.configuration'
)}
</Typography>
<div
className={`${classes.flex} ${classes.paddedTop}`}
style={{ gap: '2.5rem', width: '98.5%', flexWrap: 'wrap' }}
>
<div className={classes.flex}>
<InputField
label={t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.legend'
)}
data-cy="queryLegend"
width="16rem"
variant="primary"
value={localQuery.legend}
onChange={(event: React.ChangeEvent<{ value: string }>) => {
setLocalQuery({
...localQuery,
legend: (event.target as HTMLInputElement).value,
});
setUpdate(true);
}}
/>
<InfoTooltip
value={t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.legendInfo'
)}
className={classes.infoIcon}
/>
</div>
<div className={classes.flex}>
<InputField
label={t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.minStep'
)}
data-cy="minStep"
width="9rem"
variant={
!validateTimeInSeconds(`${localQuery.minstep}s`)
? 'error'
: 'primary'
}
value={`${localQuery.minstep}s`}
onChange={(event: React.ChangeEvent<{ value: string }>) => {
setLocalQuery({
...localQuery,
minstep: (event.target as HTMLInputElement).value.split(
's'
)[0],
});
setUpdate(true);
}}
/>
<InfoTooltip
value={t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.minStepInfo'
)}
className={classes.infoIcon}
/>
</div>
<div className={classes.flex}>
<InputField
label={t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.format'
)}
data-cy="dataFormat"
width="9rem"
variant="primary"
disabled
value={t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.timeSeries'
)}
/>
<InfoTooltip
value={t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.formatInfo'
)}
className={classes.infoIcon}
/>
</div>
</div>
<div
className={`${classes.flex} ${classes.paddedTop}`}
style={{ gap: '2.5rem' }}
>
<div className={classes.flex}>
<FormControl
variant="outlined"
className={classes.formControl}
style={{ width: '12.5rem' }}
color="primary"
>
<InputLabel className={classes.selectTextLabel}>
{t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.graph'
)}
</InputLabel>
<Select
value={
localQuery.line
? t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.lineGraph'
)
: t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.areaGraph'
)
}
onChange={(event) => {
const line =
(event.target.value as string) ===
t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.areaGraph'
);
setLocalQuery({
...localQuery,
line,
close_area: !line,
});
setUpdate(true);
}}
label={t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.graph'
)}
className={classes.selectText}
>
<MenuItem
key={`${t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.lineGraph'
)}`}
value={t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.lineGraph'
)}
>
{t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.lineGraph'
)}
</MenuItem>
<MenuItem
key={`${t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.areaGraph'
)}`}
value={t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.areaGraph'
)}
>
{t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.areaGraph'
)}
</MenuItem>
</Select>
</FormControl>
<InfoTooltip
value={t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.graphInfo'
)}
className={classes.infoIcon}
/>
</div>
<div className={classes.flex}>
<FormControl
variant="outlined"
className={classes.formControl}
style={{ width: '8.5rem' }}
color="primary"
>
<InputLabel className={classes.selectTextLabel}>
{t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.resolution'
)}
</InputLabel>
<Select
value={localQuery.resolution}
onChange={(event) => {
setLocalQuery({
...localQuery,
resolution: event.target.value as string,
});
setUpdate(true);
}}
label={t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.resolution'
)}
className={classes.selectText}
>
{resolutions.map((resolution: string) => (
<MenuItem key={resolution} value={resolution}>
{resolution}
</MenuItem>
))}
</Select>
</FormControl>
<InfoTooltip
value={t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.resolutionInfo'
)}
className={classes.infoIcon}
/>
</div>
</div>
</div>
</AccordionDetails>
</Accordion>
</div>
);
};
export default QueryEditor;

View File

@ -0,0 +1,90 @@
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles((theme) => ({
root: {
width: '100%',
display: 'inline-block',
background: theme.palette.background.paper,
padding: theme.spacing(2, 2, 0),
},
query: {
flexDirection: 'row-reverse',
background: theme.palette.cards.header,
borderRadius: '0.32rem',
},
queryContainer: {
position: 'relative',
width: '100%',
background: theme.palette.background.paper,
padding: theme.spacing(3.5, 5, 1),
},
queryTitle: {
fontSize: '1rem',
lineHeight: '150%',
letterSpacing: '0.1176px',
color: theme.palette.highlight,
},
iconButton: {
backgroundColor: 'transparent !important',
cursor: 'pointer',
display: 'flex',
padding: theme.spacing(0.35, 0.85, 0.15),
},
icon: {
width: '1rem',
height: '1rem',
},
// Form Select Properties
formControl: {
height: '3.25rem',
},
selectTextLabel: {
color: theme.palette.border.main,
'&.Mui-focused': {
color: theme.palette.primary.main,
},
},
selectText: {
height: '3.25rem',
},
infoIcon: {
margin: theme.spacing(1.75, 0, 0, 0.5),
},
formLabel: {
color: theme.palette.text.hint,
'&.Mui-focused': {
color: theme.palette.primary.main,
},
},
flex: {
display: 'flex',
},
summaryHeader: {
justifyContent: 'space-between',
width: '100%',
},
configHeader: {
fontSize: '1rem',
lineHeight: '140%',
paddingTop: theme.spacing(3.5),
},
paddedTop: {
paddingTop: theme.spacing(2.5),
},
}));
export default useStyles;

View File

@ -0,0 +1,403 @@
import {
AppBar,
Avatar,
FormControlLabel,
Switch,
Tabs,
TextField,
Typography,
useTheme,
} from '@material-ui/core';
import { Autocomplete } from '@material-ui/lab';
import { ButtonFilled, ButtonOutlined, EditableText } from 'litmus-ui';
import moment from 'moment';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { v4 as uuidv4 } from 'uuid';
import { StyledTab, TabPanel } from '../../../../../../../components/Tabs';
import {
PanelDetails,
PromQueryDetails,
} from '../../../../../../../models/dashboardsData';
import {
ApplicationMetadata,
PanelOption,
} from '../../../../../../../models/graphql/dashboardsDetails';
import { promInput } from '../../../../../../../models/graphql/prometheus';
import { ReactComponent as AddQueryIcon } from '../../../../../../../svg/add-query.svg';
import { ReactComponent as SwitchIcon } from '../../../../../../../svg/switch-checked.svg';
import { getPromQueryInput } from '../../../../../../../utils/promUtils';
import Graph from './Graph';
import QueryEditor from './QueryEditor';
import useStyles from './styles';
interface Option {
name: string;
[index: string]: any;
}
interface QueryEditingWizardProps {
panelVars: PanelDetails;
selectedApps: ApplicationMetadata[];
seriesList: Array<Option>;
panelGroupsList: Array<Option>;
index: number;
handleUpdatePanel: (panelVars: PanelDetails, index: number) => void;
handleDeletePanel: (index: number) => void;
handleDiscardChanges: (index: number) => void;
}
interface Update {
triggerUpdate: boolean;
graph: boolean;
}
const QueryEditingWizard: React.FC<QueryEditingWizardProps> = ({
panelVars,
selectedApps,
seriesList,
panelGroupsList,
index,
handleUpdatePanel,
handleDeletePanel,
handleDiscardChanges,
}) => {
const classes = useStyles();
const theme = useTheme();
const { t } = useTranslation();
const [tabValue, setTabValue] = React.useState<number>(0);
const [panelInfo, setPanelInfo] = useState<PanelDetails>({ ...panelVars });
const [update, setUpdate] = useState<Update>({
triggerUpdate: false,
graph: false,
});
const [settings, setSettings] = useState<boolean>(true);
const [prometheusQueryData, setPrometheusQueryData] =
React.useState<promInput>({
ds_details: {
url: panelInfo.ds_url ?? '',
start: `${
new Date(
moment.unix(Math.round(new Date().getTime() / 1000) - 900).format()
).getTime() / 1000
}`,
end: `${
new Date(
moment.unix(Math.round(new Date().getTime() / 1000)).format()
).getTime() / 1000
}`,
},
queries: getPromQueryInput(
panelInfo.prom_queries.filter((query) => !query.hidden),
900,
false
),
});
useEffect(() => {
if (update.triggerUpdate) {
handleUpdatePanel(panelInfo, index);
if (update.graph) {
setPrometheusQueryData({
ds_details: {
url: panelInfo.ds_url ?? '',
start: `${
new Date(
moment
.unix(Math.round(new Date().getTime() / 1000) - 900)
.format()
).getTime() / 1000
}`,
end: `${
new Date(
moment.unix(Math.round(new Date().getTime() / 1000)).format()
).getTime() / 1000
}`,
},
queries: getPromQueryInput(
panelInfo.prom_queries.filter((query) => !query.hidden),
900,
false
),
});
}
setUpdate({ triggerUpdate: false, graph: false });
}
}, [update]);
const handleAddQuery = () => {
const existingQueries: PromQueryDetails[] = panelInfo.prom_queries;
existingQueries.push({
hidden: false,
queryid: uuidv4(),
prom_query_name: '',
legend: '',
resolution: '1/2',
minstep: '5',
line: true,
close_area: false,
});
setPanelInfo({ ...panelInfo, prom_queries: existingQueries });
setUpdate({ triggerUpdate: true, graph: true });
};
const handleDeleteQuery = (index: number) => {
const existingQueries: PromQueryDetails[] = panelInfo.prom_queries;
if (index !== 0 || existingQueries.length > 1) {
existingQueries.splice(index, 1);
setPanelInfo({ ...panelInfo, prom_queries: existingQueries });
setUpdate({ triggerUpdate: true, graph: true });
}
};
const handleShowAndHideQuery = (index: number) => {
const existingQueries: PromQueryDetails[] = panelInfo.prom_queries;
existingQueries[index].hidden = !existingQueries[index].hidden;
setPanelInfo({ ...panelInfo, prom_queries: existingQueries });
setUpdate({ triggerUpdate: true, graph: true });
};
const handleUpdateQuery = (query: PromQueryDetails, index: number) => {
const existingQueries: PromQueryDetails[] = panelInfo.prom_queries;
existingQueries[index] = query;
setPanelInfo({ ...panelInfo, prom_queries: existingQueries });
setUpdate({ triggerUpdate: true, graph: true });
};
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
setTabValue(newValue);
};
return (
<div className={classes.root}>
<div className={`${classes.flexBetween} ${classes.header}`}>
<div className={classes.flex}>
<Autocomplete
defaultValue={{ name: panelInfo.panel_group_name ?? '' }}
freeSolo
id={`pg-${panelInfo.panel_group_name}`}
options={panelGroupsList}
getOptionLabel={(option: Option) => option.name}
style={{ width: '10.3rem' }}
renderInput={(params) => (
<TextField {...params} variant="standard" size="small" />
)}
onChange={(event, value, reason) => {
setPanelInfo({
...panelInfo,
panel_group_name: value
? reason === 'create-option'
? (value as string)
: (value as Option).name
: '',
panel_group_id:
reason === 'create-option'
? ''
: panelInfo.panel_group_id ?? '',
});
setUpdate({ triggerUpdate: true, graph: false });
}}
/>
<Typography className={classes.divider}> / </Typography>
<EditableText
defaultValue={panelInfo.panel_name}
id="name"
onSave={(value) => {
setPanelInfo({ ...panelInfo, panel_name: value });
setUpdate({ triggerUpdate: true, graph: false });
}}
/>
</div>
<div
className={`${classes.flex} ${classes.controlButtons}`}
style={{ gap: '1rem' }}
>
<ButtonOutlined
onClick={() => {
setSettings(!settings);
}}
className={classes.iconButton}
>
<img
src="/icons/query-settings.svg"
alt="Settings icon"
className={classes.icon}
/>
</ButtonOutlined>
<ButtonOutlined
onClick={() => {
handleDeletePanel(index);
}}
className={`${classes.iconButton} ${classes.deleteButton}`}
>
<img
src="/icons/delete.svg"
alt="Delete icon"
className={classes.icon}
/>
</ButtonOutlined>
</div>
</div>
<Graph panelVars={panelVars} prometheusQueryData={prometheusQueryData} />
{settings && (
<>
<div className={classes.editSection}>
<AppBar
position="static"
color="default"
className={classes.appBar}
>
<Tabs
value={tabValue}
onChange={handleChange}
TabIndicatorProps={{
style: {
backgroundColor: theme.palette.primary.main,
},
}}
variant="scrollable"
scrollButtons="auto"
>
<StyledTab
label={
panelInfo.prom_queries.length > 1
? t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.queries'
)
: t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.query'
)
}
icon={
<Avatar className={classes.avatar}>
<Typography className={classes.queryCount}>
{panelInfo.prom_queries.length}
</Typography>
</Avatar>
}
/>
<StyledTab
label={t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.visualization'
)}
/>
</Tabs>
</AppBar>
<TabPanel value={tabValue} index={0}>
{panelInfo.prom_queries.map((prom_query, index) => (
<QueryEditor
index={index}
key={`query-editor-${prom_query.queryid}`}
promQuery={prom_query}
selectedApps={selectedApps}
dsURL={panelInfo.ds_url ?? ''}
seriesList={seriesList}
handleDeleteQuery={handleDeleteQuery}
handleShowAndHideQuery={handleShowAndHideQuery}
handleUpdateQuery={handleUpdateQuery}
/>
))}
</TabPanel>
<TabPanel value={tabValue} index={1}>
<div className={classes.switches}>
<FormControlLabel
control={
<Switch
checked={panelInfo.panel_options.points}
onChange={(
event: React.ChangeEvent<HTMLInputElement>
) => {
const updatedPanelOptions: PanelOption = {
...panelInfo.panel_options,
points: event.target.checked,
};
setPanelInfo({
...panelInfo,
panel_options: updatedPanelOptions,
});
setUpdate({ triggerUpdate: true, graph: true });
}}
name={`${t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.points'
)}`}
icon={<SwitchIcon />}
checkedIcon={<SwitchIcon />}
/>
}
label={`${t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.points'
)}`}
labelPlacement="start"
/>
<FormControlLabel
control={
<Switch
checked={panelInfo.panel_options.grids}
onChange={(
event: React.ChangeEvent<HTMLInputElement>
) => {
const updatedPanelOptions: PanelOption = {
...panelInfo.panel_options,
grids: event.target.checked,
};
setPanelInfo({
...panelInfo,
panel_options: updatedPanelOptions,
});
setUpdate({ triggerUpdate: true, graph: true });
}}
name={`${t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.grids'
)}`}
icon={<SwitchIcon />}
checkedIcon={<SwitchIcon />}
/>
}
label={`${t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.grids'
)}`}
labelPlacement="start"
/>
</div>
</TabPanel>
</div>
<div
className={`${classes.flexBetween} ${classes.topMargin} ${
tabValue === 0 ? '' : classes.flexEnd
}`}
>
{tabValue === 0 && (
<ButtonOutlined
onClick={handleAddQuery}
startIcon={<AddQueryIcon />}
>
<Typography>
{t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.addQuery'
)}
</Typography>
</ButtonOutlined>
)}
<ButtonFilled
onClick={() => {
handleDiscardChanges(index);
}}
className={classes.discardButton}
>
<Typography>
{t(
'analyticsDashboard.applicationDashboards.tuneTheQueries.discardChanges'
)}
</Typography>
</ButtonFilled>
</div>
</>
)}
</div>
);
};
export default QueryEditingWizard;

View File

@ -0,0 +1,84 @@
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles((theme) => ({
root: {
background: theme.palette.background.paper,
padding: theme.spacing(5, 2.5, 0),
},
appBar: {
background: 'transparent',
boxShadow: 'none',
paddingLeft: theme.spacing(1.5),
},
icon: {
margin: theme.spacing(0, 1),
width: '1.25rem',
},
iconButton: {
minWidth: 0,
background: 'transparent',
padding: theme.spacing(0.25, 0.5),
},
deleteButton: {
borderColor: theme.palette.error.main,
},
queryCount: {
fontWeight: 500,
fontSize: '1rem',
lineHeight: '140%',
textAlign: 'center',
color: theme.palette.text.hint,
},
editSection: {
paddingTop: theme.spacing(3),
},
avatar: {
width: '1.75rem',
height: '1.75rem',
marginLeft: theme.spacing(1.5),
background: theme.palette.cards.header,
},
discardButton: {
background: theme.palette.error.main,
'&:hover': {
background: theme.palette.error.main,
cursor: 'pointer',
},
},
flexBetween: {
display: 'flex',
justifyContent: 'space-between',
width: '100%',
padding: theme.spacing(0, 2),
},
topMargin: {
marginTop: theme.spacing(2),
},
flexEnd: {
justifyContent: 'flex-end',
},
switches: {
display: 'flex',
gap: '3.375rem',
padding: theme.spacing(5, 0),
},
divider: {
fontWeight: 500,
fontSize: '1.125rem',
lineHeight: '1.375rem',
letterSpacing: '0.1142px',
color: theme.palette.text.hint,
padding: theme.spacing(0, 1),
},
flex: {
display: 'flex',
},
controlButtons: {
marginTop: theme.spacing(-1.5),
},
header: {
padding: theme.spacing(1, 2, 4),
},
}));
export default useStyles;

Some files were not shown because too many files have changed in this diff Show More