mirror of
https://github.com/amir20/dozzle.git
synced 2026-08-07 15:38:58 +00:00
Compare commits
854 Commits
v8.7.0
...
issue-4348
| Author | SHA1 | Date | |
|---|---|---|---|
| f794a6fa99 | |||
| 56d53d037d | |||
| 8599b602dc | |||
| 4b3e44fcdc | |||
| 51fce48137 | |||
| c1432c33a2 | |||
| 22d659f509 | |||
| 9c529af633 | |||
| 9988271490 | |||
| 05f76c4f57 | |||
| 20c1adbd7e | |||
| d44ab349b9 | |||
| 21367bcc36 | |||
| 6ece0df082 | |||
| b087f30fb9 | |||
| 5e65e2fca5 | |||
| 2303fbdfd3 | |||
| 078f4c4064 | |||
| 5be1a55d33 | |||
| 09b1bae3cd | |||
| 2a0037a667 | |||
| e00752b0bb | |||
| 497e474471 | |||
| 753d45d7af | |||
| 9a2d6fc6e8 | |||
| 4dcaa43597 | |||
| b51ec5bfa7 | |||
| 8c8ac09521 | |||
| 2537e16b6c | |||
| a8a76e29c9 | |||
| 65b2ac9181 | |||
| ddc4e310a8 | |||
| 0a2afcf93c | |||
| 41cc1eb2ff | |||
| 2a63d300a4 | |||
| fb9265204d | |||
| 41d46f95b3 | |||
| d6ad628769 | |||
| 8c4b103468 | |||
| 9ee66dbe80 | |||
| 55aeb46262 | |||
| 1b260019a1 | |||
| e07d97a28a | |||
| 7b53cffdae | |||
| d630056a61 | |||
| 1f712b56e0 | |||
| c199e1fa28 | |||
| 60c8f7a255 | |||
| 6bafcc6f77 | |||
| 529fb3ad6d | |||
| 8000b6c14e | |||
| b74dc9f58a | |||
| 67156b501f | |||
| fd69eae2ea | |||
| 9530bfeadc | |||
| 1d6b4293e7 | |||
| 940bcde6c6 | |||
| 836c179b4c | |||
| 91152d7a94 | |||
| 109c0e50e0 | |||
| a9cfa97704 | |||
| 4ac7bd0f05 | |||
| 512293968e | |||
| e443dd4c9b | |||
| 835ca9fbc6 | |||
| cbf30f3ae0 | |||
| 523761c424 | |||
| bba95b98a6 | |||
| 6d01128092 | |||
| 4b2235e0da | |||
| b361f0b772 | |||
| d84cc574e3 | |||
| bafcbf6d66 | |||
| 80df836f1d | |||
| 1b36d2c79d | |||
| b09d73736b | |||
| 604133b591 | |||
| 065dd0aea0 | |||
| b1cbd7c23e | |||
| 0b8b60afd9 | |||
| da32dc9171 | |||
| 653fc8878a | |||
| 3abe70b402 | |||
| 6097bd49b7 | |||
| 8462ee9454 | |||
| 8e6f7f16ba | |||
| 4605a4a59d | |||
| c7615d01a3 | |||
| aba8746dd0 | |||
| 9e7de63f26 | |||
| 313cde3ad1 | |||
| bf0148d131 | |||
| 72d1ecbcc6 | |||
| 50ce41eca3 | |||
| cffd4bd4e3 | |||
| 0f1e9cf0e6 | |||
| a761129aff | |||
| 16de645a89 | |||
| 1c6f30bcd3 | |||
| 9f29aa5923 | |||
| c2a17888a7 | |||
| 176c173ac7 | |||
| ce436d9697 | |||
| b752afb352 | |||
| 50dc54f4bb | |||
| e6eb7eeebd | |||
| 0cd62745f6 | |||
| 911f785e2c | |||
| 074f402d0f | |||
| 9485fa738b | |||
| bb56154811 | |||
| 78e63c39a0 | |||
| c1c8e8cccf | |||
| f9bc915ace | |||
| 938c6229f8 | |||
| b945c35bfa | |||
| 47adf9845a | |||
| 6951c1cc71 | |||
| 9dc27fa06c | |||
| f5786ba388 | |||
| b7b9ea7c6f | |||
| d7b6e9ee50 | |||
| 34f68849f4 | |||
| fa78467698 | |||
| 573ee5ed03 | |||
| cb6267f9ae | |||
| 3aa3b879de | |||
| 79bf0f01f9 | |||
| 6b442d26a9 | |||
| 67348ab514 | |||
| cb9ab3f944 | |||
| 0a722bd2da | |||
| 3195bc0d80 | |||
| 7bf455ae6b | |||
| 285a8bf251 | |||
| 65d690074c | |||
| 264af62bff | |||
| c3ac804e7a | |||
| e8e1c3ea6b | |||
| 29f06eb00d | |||
| 020342a1bd | |||
| 3b0b3ed5fc | |||
| 6b121633e9 | |||
| 443084964a | |||
| dc1314ddc3 | |||
| c4060b2206 | |||
| 5675389049 | |||
| bc3c812c74 | |||
| 5aea48f3ca | |||
| ab86a6c59e | |||
| f667e5a271 | |||
| 58146d1ac5 | |||
| a6b65a1341 | |||
| a6003e4c1c | |||
| 4e59bb2f8f | |||
| 220dcd9416 | |||
| fb359cb9d5 | |||
| 395d8641ad | |||
| 84ec399e50 | |||
| 2b0c9124d7 | |||
| e75b4d03bc | |||
| 5f24657b41 | |||
| 54799b420c | |||
| 23ad32d05c | |||
| 6e4a5238d3 | |||
| a1cedd4b16 | |||
| 84edb6bdab | |||
| a8c673544f | |||
| b2c3363659 | |||
| 3f376a0bce | |||
| 16fc75b563 | |||
| 7ed0ec498d | |||
| 11d12d1f6a | |||
| e88c412d62 | |||
| 96a02bf213 | |||
| 35b2c919cc | |||
| 85396d7631 | |||
| a3a6a8ec5c | |||
| 1168c03fc9 | |||
| 10597521de | |||
| da7730cffd | |||
| ed5090a9ec | |||
| c97a9cc34f | |||
| 31d7fe5c67 | |||
| ed8ce856ba | |||
| 940ba30633 | |||
| a4b0b05978 | |||
| 146ff180e0 | |||
| db4f11c68e | |||
| e702b62324 | |||
| cf97100b71 | |||
| b5cc4bd514 | |||
| f6e3d487ac | |||
| a2cc759538 | |||
| cfd757e876 | |||
| 01c564228b | |||
| 0b401aacd1 | |||
| d7335a8cbe | |||
| 6df66dade9 | |||
| ae083e955a | |||
| 99faac6e8e | |||
| 16495a66a0 | |||
| 03a2eb2f3b | |||
| bf18fdd1d8 | |||
| 912595f781 | |||
| 2a5a0b0c55 | |||
| 4e9667e8cf | |||
| 8130889484 | |||
| 013f7c5cde | |||
| 37c1fd032f | |||
| cd4f98fd66 | |||
| b78dab6da1 | |||
| 7be0515648 | |||
| af93918f8c | |||
| 9fa0b5256c | |||
| f037b81c92 | |||
| a97d976c4f | |||
| 6f87c42d2b | |||
| 1064af3175 | |||
| 9939b0001e | |||
| a54dd42531 | |||
| ba9faa7691 | |||
| e044ecd4a1 | |||
| da2d554bee | |||
| 9f0791a38b | |||
| eb20f9e4fa | |||
| 3ca6495404 | |||
| 44d30b8451 | |||
| 1f4410f257 | |||
| bd3e789d4c | |||
| 5d32e04af8 | |||
| 26c85a42b5 | |||
| 4aa06778d7 | |||
| 5755afcedd | |||
| 482c5ae445 | |||
| 8506f6123e | |||
| 4d5f94c2bb | |||
| 90ca59d283 | |||
| 84e608eab1 | |||
| 1dddbec30a | |||
| 0d9ff396d6 | |||
| dc9c6b727d | |||
| 4bf342da87 | |||
| 44c02f6676 | |||
| f843635060 | |||
| bcc8506d8c | |||
| c106808c1d | |||
| f7ff41881a | |||
| b58648d562 | |||
| 6d829b9cae | |||
| 9250db2c7e | |||
| 0c41da9e65 | |||
| 0c25f0f82e | |||
| 500ffa6b5b | |||
| 0d61493e29 | |||
| f8db11a4df | |||
| f596ebc5f3 | |||
| 46b48b0f1f | |||
| a80337177f | |||
| b6cf75020f | |||
| 135fffabbc | |||
| d0fbebea30 | |||
| ae058d3b09 | |||
| 573afcfd3b | |||
| 5de0e23b9e | |||
| d5b5b92701 | |||
| def47b560e | |||
| 3916e3aec8 | |||
| 743678da33 | |||
| 7291448d90 | |||
| 99d0af05d5 | |||
| a87908d957 | |||
| 8d38f36745 | |||
| 100204022d | |||
| 35dea10a1c | |||
| 176d41aff3 | |||
| cd9a771cab | |||
| 633d794539 | |||
| c1955a3301 | |||
| 3a4f0a6e79 | |||
| 2c50d870d2 | |||
| 229758c1db | |||
| 6ee4a8594c | |||
| e2b35b3c8c | |||
| 2d39629de1 | |||
| 61af442264 | |||
| 8dde2fe9ac | |||
| da83f8af11 | |||
| eadf6c98fe | |||
| 931c85afb6 | |||
| a47be8730a | |||
| bcb859509b | |||
| d98000b35a | |||
| a8dced5c2b | |||
| 3c2a2119d8 | |||
| a5ed9b65d5 | |||
| e01baf7119 | |||
| 064d5c21f2 | |||
| fae248b066 | |||
| f22c51690b | |||
| 373f9ae025 | |||
| d4dab0ad1a | |||
| 098f032d03 | |||
| 9db559c9ce | |||
| 7257e35f1b | |||
| 1c20aba5d7 | |||
| 252d865460 | |||
| 02c28fd001 | |||
| b39f2ae6a8 | |||
| 7e2dd1c69e | |||
| 7e5b413c48 | |||
| ec513adde2 | |||
| 186b4504fc | |||
| 9dc286643e | |||
| 553cfd558d | |||
| 31174a5214 | |||
| 515849e72a | |||
| f442635f55 | |||
| a8f2dc17d6 | |||
| 1faa84abc5 | |||
| bdcb1673e1 | |||
| 4de23926eb | |||
| d4defe680e | |||
| 4c2b459fa8 | |||
| aeadae0d30 | |||
| f6f1b5207e | |||
| cfe7285bf7 | |||
| defaba29c2 | |||
| 2aad18d729 | |||
| 9c52d604bc | |||
| 7f936a9a38 | |||
| 0c9065a210 | |||
| b70c2da290 | |||
| 804199aa9a | |||
| 50c3608a5e | |||
| 65b68ad907 | |||
| fd663b9c0e | |||
| 7a0296fc30 | |||
| b8a3325ea7 | |||
| 0ade181ab2 | |||
| 7d60d951ff | |||
| 66b0dfe280 | |||
| c273cb5320 | |||
| 570ef771dd | |||
| 07014470e9 | |||
| bd3c2f5c24 | |||
| 6f45faf27a | |||
| e94a245440 | |||
| daa02dd899 | |||
| 6a27f9f367 | |||
| 9d7b8f26a6 | |||
| 82576ae452 | |||
| 2ca71a49ba | |||
| 3abeeec814 | |||
| 5619201886 | |||
| 0c06d25ae3 | |||
| 7a070b90ed | |||
| 9c70416aec | |||
| 68ca6d3040 | |||
| 711745eb1f | |||
| c172f6f73f | |||
| 7941bd7b6f | |||
| 595e39a139 | |||
| 8e074df145 | |||
| 54c6e1ff93 | |||
| 10e819fc09 | |||
| c16cfa2c71 | |||
| 56eebf784a | |||
| 5c206b6de3 | |||
| 2add15bf42 | |||
| 7ff1ccdffb | |||
| b961b11499 | |||
| 514c690c7a | |||
| e8b3e72265 | |||
| e8d8b72406 | |||
| 0c962eec43 | |||
| c7f318cc5c | |||
| 0e8452c146 | |||
| 2d3f6e2b1b | |||
| 92665a76de | |||
| 78ace3178b | |||
| a13b7b0e9d | |||
| 4da0486295 | |||
| 11bc1c7fc2 | |||
| f001b48fa3 | |||
| baf9f74fd0 | |||
| f817452fb5 | |||
| 95b27f60cb | |||
| 9daa3459ad | |||
| 01343b74bf | |||
| 5433939690 | |||
| effeb5aa52 | |||
| 7c59060e1e | |||
| 85c42eed11 | |||
| 433c0eb59e | |||
| 044ef7a2b2 | |||
| 847c4ee115 | |||
| 4f912b6fdc | |||
| 5616c07bad | |||
| 31c51f1939 | |||
| 10705f05a6 | |||
| 3645100402 | |||
| 757c1385f7 | |||
| ccfab708d4 | |||
| 55168fd81a | |||
| bea1876c7c | |||
| ae7eee9bca | |||
| d98b84e24a | |||
| 18c87256cc | |||
| e96631930e | |||
| 76b1229341 | |||
| 440f9394d6 | |||
| ec792318e6 | |||
| 16db90b660 | |||
| eb22752b9a | |||
| 642ab45ef7 | |||
| 12fadb9348 | |||
| 1cf178e37d | |||
| 061cd0c445 | |||
| e8f796a7d6 | |||
| 78f94f1f16 | |||
| cc980361fd | |||
| 736ad8ed5b | |||
| d0dbd2051e | |||
| 40a17b4088 | |||
| b3da3d70a9 | |||
| 28588fdfd2 | |||
| a33f7bf3b2 | |||
| febdbff0de | |||
| 0fee11e036 | |||
| b768cbfc89 | |||
| d43de49b6d | |||
| facc137a93 | |||
| a052f55dc6 | |||
| 481f2bd592 | |||
| b33ae4fa4e | |||
| f35c472088 | |||
| 7455a4c53c | |||
| 84101620f8 | |||
| 51695b486e | |||
| 2f84190faa | |||
| ebcedb2a67 | |||
| a514168e45 | |||
| 6bb3777a64 | |||
| d22e6f9014 | |||
| b779998476 | |||
| 70c23b0b1e | |||
| ddff265023 | |||
| 52edcce8d8 | |||
| 0f75010b35 | |||
| 5e228756f3 | |||
| 3775170457 | |||
| 42edc4c623 | |||
| f363b6f366 | |||
| df7e461876 | |||
| f691a22d05 | |||
| 9c2e03727b | |||
| 2177320259 | |||
| 2a2f6bc9c6 | |||
| e4fab590b9 | |||
| cf750b6e39 | |||
| 8e2ae35ad2 | |||
| 8b4f5ea32d | |||
| 2fc848254d | |||
| fa2dc863ea | |||
| 11a78629ef | |||
| 8b058cae07 | |||
| 8c4fcc6a7a | |||
| 9b5aaad109 | |||
| aad9287b97 | |||
| 1e6b7da245 | |||
| b8bc681f7a | |||
| c646281e46 | |||
| 27478eb652 | |||
| 492c5bfd32 | |||
| 397f42a223 | |||
| ddcc5ccbe1 | |||
| 9ae7c4cbd8 | |||
| 076f5d0fc9 | |||
| b8a8781a9b | |||
| d5abf1868a | |||
| d5a2841e02 | |||
| 410f5f305c | |||
| 48d9958e0c | |||
| ea2132efc9 | |||
| 7a6914c6cf | |||
| da66540952 | |||
| f863fc1bd3 | |||
| 91b9c26c3a | |||
| ae483639bb | |||
| 38383e8924 | |||
| c66bbc520c | |||
| 7c2b7a5dcb | |||
| 0c96821f15 | |||
| d1a225adc1 | |||
| 3a7efe9ada | |||
| 3e3bb02b5f | |||
| c3ee2da33c | |||
| a488e2e7ee | |||
| 46fdb88b81 | |||
| c88429c854 | |||
| 9e2d053ec8 | |||
| 97ece4c5ac | |||
| 4da50bbbd9 | |||
| ababa789c3 | |||
| 60100dfdbb | |||
| 0370759f69 | |||
| 1b01710aa8 | |||
| 0ffdd53568 | |||
| d9c9d20fff | |||
| e0797d7675 | |||
| 25d8ba413a | |||
| 80bbb317aa | |||
| c1c61c0ce6 | |||
| 792a9f1187 | |||
| a3a6c03f11 | |||
| 46b4fc0994 | |||
| 4878144592 | |||
| 99f203a23e | |||
| 211a442147 | |||
| 02aafac2a9 | |||
| 3b260bef27 | |||
| fe0902e62a | |||
| e4a42125c5 | |||
| db2b7b12a3 | |||
| d6b262aead | |||
| 917489838d | |||
| cefee41857 | |||
| 8455e2743d | |||
| a5cc1f7ca0 | |||
| 1ffc338720 | |||
| 19bb865913 | |||
| bb4dca2386 | |||
| 808439365f | |||
| c1038c0a9e | |||
| 15e410a874 | |||
| 5049a53a95 | |||
| c86d108997 | |||
| 604c4b9b16 | |||
| 64b03d7b92 | |||
| a533533d4c | |||
| 2d50820932 | |||
| 3190868268 | |||
| 2c031da510 | |||
| 58ececc42a | |||
| 7be55cbc08 | |||
| 90b90ceea4 | |||
| bc842389f9 | |||
| cf9bf4dcd5 | |||
| 3eec803e45 | |||
| e332b4225b | |||
| 538d16a5b7 | |||
| 561081ebf0 | |||
| c3d2e7ac09 | |||
| 90808aff68 | |||
| 77bfb43b6d | |||
| 49881c2dfe | |||
| 3f901769cb | |||
| 26f7704edb | |||
| 3686f5c54a | |||
| aabd9dd994 | |||
| 52e0be0ac6 | |||
| bb0ffbfa04 | |||
| 4022b510e2 | |||
| 21fb03c9d0 | |||
| 392f480c76 | |||
| c9418d4b32 | |||
| 14b14e6210 | |||
| 3d5b279a1b | |||
| aca3e3a219 | |||
| 241d349d39 | |||
| b098dcaef0 | |||
| 7cf67eb99a | |||
| 869d5e5002 | |||
| 392dfafc3d | |||
| 21fe5b84f2 | |||
| 65fa04c877 | |||
| 3a352a7320 | |||
| 3bdc9c04f8 | |||
| 6e3c811974 | |||
| ddff9e96a2 | |||
| 1daf813e1b | |||
| 09a169851b | |||
| 1d5ddee1ae | |||
| b7e9871382 | |||
| 9e028b9916 | |||
| 78c0e5fd7f | |||
| 4861503488 | |||
| 115b0e475d | |||
| 6d928815df | |||
| 2a70d0b458 | |||
| 8c9c9a83af | |||
| 571de4084e | |||
| 5abf7e4a9d | |||
| d580e9447b | |||
| 877faaa740 | |||
| 1d4698d77a | |||
| 13da2a4222 | |||
| 209ce312d4 | |||
| 6da36c4c0d | |||
| d9d93a8147 | |||
| eba87fa49e | |||
| 7ae409981c | |||
| 20fb7984d2 | |||
| 17c15b65a2 | |||
| 9ce736a448 | |||
| 10c95124e4 | |||
| 5e02142f77 | |||
| f9585cc656 | |||
| 6da7f6dffd | |||
| 2de739141d | |||
| 585e1edfee | |||
| bf81852003 | |||
| 5f73b41c57 | |||
| 9f7b17f4ec | |||
| 6c292be943 | |||
| 21ae6eda8b | |||
| b732844e7b | |||
| 1b38f37ac2 | |||
| c20ce29f5c | |||
| 47e64bb57a | |||
| b9a5262257 | |||
| 47204b33c4 | |||
| 162f9ea580 | |||
| 2381a8b8a3 | |||
| 8fbbfaf043 | |||
| 3d077b9503 | |||
| 68ea86c55b | |||
| ad5c1c4452 | |||
| 0efd98e999 | |||
| ff72431a72 | |||
| 6d2ed53629 | |||
| 992e1a27ae | |||
| 2378a1f115 | |||
| 3b122564d9 | |||
| a783f7060c | |||
| 76e5c63436 | |||
| 5a17a07b8f | |||
| d1fc9b750b | |||
| 87a59939dd | |||
| 3a159cb89b | |||
| 84ec46c400 | |||
| 0c4ca6d605 | |||
| db5d36c89b | |||
| 8cdacf608a | |||
| 78487a553c | |||
| 8da67482d8 | |||
| 60914f3208 | |||
| 0dc2a391df | |||
| 4cfd3d0cfa | |||
| 9752504097 | |||
| f08f2455de | |||
| b2ea1c592a | |||
| 6f89a1e70b | |||
| 93ce48ebf3 | |||
| 0412581b6f | |||
| 285d0ef0f4 | |||
| be313a68a3 | |||
| 14ddd1c4e8 | |||
| 368646a7a3 | |||
| 691a89660e | |||
| d03844b488 | |||
| 3f860805f5 | |||
| e8eef82b1c | |||
| e7e52cbe80 | |||
| cd9409f7e8 | |||
| d455c3eed8 | |||
| 35fd47d157 | |||
| 58d6848084 | |||
| 11e1285158 | |||
| 43e3f4cd9d | |||
| 5b822bdcfb | |||
| 0f1a916ff5 | |||
| 5766410892 | |||
| 7c061a5ef1 | |||
| 05f55538b4 | |||
| e8752c2e25 | |||
| 3cb7418ede | |||
| 37eccc5724 | |||
| b29e77e05d | |||
| a389c31b68 | |||
| 45b15ccc1b | |||
| 45ac8740f0 | |||
| 2f0dd4f36f | |||
| 19ebbccc29 | |||
| 90459ff3d5 | |||
| 5d57439d3d | |||
| 5c33f4437e | |||
| cbbe5f7571 | |||
| d79c517425 | |||
| 130defa367 | |||
| f8e7dafd8a | |||
| 81f24d1c26 | |||
| fdc3f06402 | |||
| a1a9031cda | |||
| 151a3efb16 | |||
| 42cc254eb7 | |||
| 917c837e90 | |||
| 756b212826 | |||
| dae2877bdb | |||
| 66cc3ddec8 | |||
| cd4e233d87 | |||
| 1b82929bc6 | |||
| e99107da90 | |||
| efa95078ac | |||
| 40015f7385 | |||
| 46fb09e2f7 | |||
| cc620ad429 | |||
| 8c6c6882cd | |||
| 64c23bc1db | |||
| 7f12f01267 | |||
| 0f9071944f | |||
| 291d4f7f37 | |||
| 2bf811d632 | |||
| 2a3f77be48 | |||
| 9518114816 | |||
| 65ef1d3c68 | |||
| f4f61a6a75 | |||
| cc76c00933 | |||
| 08e5deba7e | |||
| 05e01852ce | |||
| 9cff809a8e | |||
| d93efedc11 | |||
| 984452c181 | |||
| 471cc19132 | |||
| 7ec053e893 | |||
| af0a8f0e2d | |||
| 52de74e595 | |||
| 4b19d13d02 | |||
| 5a532b50e3 | |||
| 91d63882e0 | |||
| a98b1059d9 | |||
| 0d51523d80 | |||
| 1a69694c16 | |||
| e5c1f884a9 | |||
| d34cf32cce | |||
| fc2a2766f7 | |||
| 4fa04d0176 | |||
| 86522ccf77 | |||
| 23ba1f6603 | |||
| 3adf56018a | |||
| d02fbae4a9 | |||
| 6479bbba10 | |||
| bd0a81f332 | |||
| f2e56f79b7 | |||
| d312871d89 | |||
| 5a5a0d329b | |||
| 2f382ba34e | |||
| 92c60b29bd | |||
| 85a304b2e8 | |||
| d3e573d03b | |||
| 9f2d172277 | |||
| 55baf9e12c | |||
| d2e1b26fb4 | |||
| 864af3c24d | |||
| c0b40da585 | |||
| 242c3e74dc | |||
| 9a73d18da6 | |||
| 74b5adad00 | |||
| a62cef7e25 | |||
| 668a0d9b19 | |||
| 39be1ddf69 | |||
| 672a5f8c73 | |||
| 289fc661f0 | |||
| 9b0e731f90 | |||
| 7ce5b9514d | |||
| f50d45dcb2 | |||
| 2081335867 | |||
| 69d380e5bd | |||
| 8400319a85 | |||
| b7bd763b54 | |||
| eefa3532c1 | |||
| 441d2d1c7b | |||
| 41188947a9 | |||
| 877f53462c | |||
| c2e3879c1f | |||
| 327ae70428 | |||
| be197ea19d | |||
| 749b19f2c1 | |||
| ec5ab3a0fe | |||
| 0ab5ad2c5d | |||
| 1448371c23 | |||
| daee5b7dc4 | |||
| 08a7d39e43 | |||
| fd13320152 | |||
| 823959f8bb | |||
| a99d711b78 | |||
| 75f089b770 | |||
| 508408c0af | |||
| 5b4ca4d766 | |||
| 036bee5621 | |||
| c36cf72506 | |||
| 129f80e30e | |||
| 3f3346d003 | |||
| ef6e0569b7 | |||
| 4c1f5b4062 | |||
| 9d244cf26b | |||
| 002dbea904 | |||
| d41f3f192a | |||
| 75fa2d1a22 | |||
| c3f6ff9c56 | |||
| 95de5a8504 | |||
| 50855a8700 | |||
| bd7f6bc886 | |||
| 7ee260311e | |||
| 011294d198 | |||
| 7ccc312190 | |||
| 484260d492 | |||
| cbdd0cc961 | |||
| cb808df400 | |||
| d4fa7ace90 | |||
| 1144bdfbd2 | |||
| 9a296ca504 | |||
| 80d502eb29 | |||
| 56a2ab11a9 | |||
| 675a4ab334 | |||
| 197ab6d4da | |||
| 9296b1f7cf | |||
| dcf261f418 | |||
| c032f788fb | |||
| b3177e95d9 | |||
| 48e509e19f | |||
| d7662c32d0 | |||
| 3487d6cb38 | |||
| feee4a07b4 | |||
| ea006a0d54 | |||
| d99964bbe7 | |||
| f3cfe4f84d | |||
| 8408f887b6 | |||
| 16d0dc6671 | |||
| 596861412d | |||
| 06136e716a | |||
| 5db3448207 | |||
| c57d6ed4f0 | |||
| b85eeeddd4 | |||
| 964429a95e | |||
| e22d54cf74 | |||
| 7bf331cf6e | |||
| d529ed7855 | |||
| 46d124b11a | |||
| d09aa5d481 | |||
| cc77c16f92 | |||
| 63be3bf2d8 | |||
| ec29c78052 | |||
| df3f04dd50 | |||
| 7af7956b7f | |||
| d21473fe0b | |||
| 4093512497 | |||
| 0ef6252158 | |||
| 5947338966 | |||
| db5e760ae3 | |||
| 1ab4464f9a | |||
| 45ea4ac095 | |||
| c6339effd1 |
@@ -0,0 +1,57 @@
|
||||
root = "."
|
||||
testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "./tmp/dozzle"
|
||||
cmd = "GOEXPERIMENT=jsonv2 go build -race -o ./tmp/dozzle ."
|
||||
delay = 1000
|
||||
exclude_dir = [
|
||||
"assets",
|
||||
"tmp",
|
||||
"data",
|
||||
"node_modules",
|
||||
"docs",
|
||||
"test-results",
|
||||
"dist",
|
||||
"e2e",
|
||||
"examples",
|
||||
]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
include_file = []
|
||||
kill_delay = "2s"
|
||||
log = "build-errors.log"
|
||||
poll = false
|
||||
poll_interval = 0
|
||||
post_cmd = []
|
||||
pre_cmd = []
|
||||
rerun = false
|
||||
rerun_delay = 500
|
||||
send_interrupt = true
|
||||
stop_on_error = true
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
main_only = false
|
||||
silent = false
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = false
|
||||
keep_scroll = true
|
||||
@@ -23,12 +23,13 @@ body:
|
||||
- Agents
|
||||
- Remote Hosts
|
||||
- Swarm Mode
|
||||
- Kubernetes
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: 📦 Dozzle version
|
||||
description: Can be found in logs or settings page.
|
||||
description: Run `docker run amir20/dozzle --version` or `curl http://<host>/api/version`.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -47,17 +48,18 @@ body:
|
||||
id: environment
|
||||
attributes:
|
||||
label: 💻 Environment
|
||||
description: Run `docker system info` and paste the output in the field below..
|
||||
description: Run `docker system info` and paste the output in the field below.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 📜 Dozzle logs are required for debugging purposes. You may need to enable debug mode. See https://dozzle.dev/guide/debugging.
|
||||
description: Drag log files or paste the logs into the text input below
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 📸 If applicable, add screenshots to help explain your bug
|
||||
description: Drag issues into the text input below
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 📜 If applicable, attach your Dozzle logs. You many need to enable debug mode. See https://dozzle.dev/guide/debugging.
|
||||
description: Drag screenshots into the text input below
|
||||
validations:
|
||||
required: false
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
buy_me_a_coffee: amirraminfar
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:base", "schedule:weekly", "group:allNonMajor", ":automergeMinor"],
|
||||
"rangeStrategy": "bump"
|
||||
"extends": ["config:recommended", "schedule:weekly", "group:allNonMajor", ":automergeMinor"],
|
||||
"rangeStrategy": "bump",
|
||||
"ignoreDeps": []
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
name: Claude Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
# Optional: Only run on specific file changes
|
||||
# paths:
|
||||
# - "src/**/*.ts"
|
||||
# - "src/**/*.tsx"
|
||||
# - "src/**/*.js"
|
||||
# - "src/**/*.jsx"
|
||||
|
||||
jobs:
|
||||
claude-review:
|
||||
# Optional: Filter by PR author
|
||||
# if: |
|
||||
# github.event.pull_request.user.login == 'external-contributor' ||
|
||||
# github.event.pull_request.user.login == 'new-developer' ||
|
||||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code Review
|
||||
id: claude-review
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
Please review this pull request and provide feedback on:
|
||||
- Code quality and best practices
|
||||
- Potential bugs or issues
|
||||
- Performance considerations
|
||||
- Security concerns
|
||||
- Test coverage
|
||||
|
||||
Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
|
||||
|
||||
Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
|
||||
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
|
||||
claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
# This is an optional setting that allows Claude to read CI results on PRs
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
|
||||
# prompt: 'Update the pull request description to include a summary of changes.'
|
||||
|
||||
# Optional: Add claude_args to customize behavior and configuration
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
|
||||
# claude_args: '--allowed-tools Bash(gh pr:*)'
|
||||
@@ -8,16 +8,18 @@ jobs:
|
||||
name: JavaScript Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
- uses: actions/setup-node@v6
|
||||
name: Install Node
|
||||
with:
|
||||
node-version: latest
|
||||
node-version: 24.13.0
|
||||
- run: corepack enable
|
||||
- run: pnpm --version
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: latest
|
||||
node-version: 24.13.0
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "**/pnpm-lock.yaml"
|
||||
- name: Install dependencies
|
||||
@@ -29,12 +31,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.22.x
|
||||
go-version: 1.25.6
|
||||
check-latest: true
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
- name: Install gRPC and Go
|
||||
@@ -48,16 +50,18 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
- uses: actions/setup-node@v6
|
||||
name: Install Node
|
||||
with:
|
||||
node-version: latest
|
||||
node-version: 24.13.0
|
||||
- run: corepack enable
|
||||
- run: pnpm --version
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: latest
|
||||
node-version: 24.13.0
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "**/pnpm-lock.yaml"
|
||||
- name: Install dependencies
|
||||
@@ -69,15 +73,16 @@ jobs:
|
||||
echo "${{ secrets.TTL_KEY }}" > shared_key.pem
|
||||
echo "${{ secrets.TTL_CERT }}" > shared_cert.pem
|
||||
- name: Build
|
||||
uses: docker/bake-action@v5
|
||||
uses: docker/bake-action@v6
|
||||
with:
|
||||
source: .
|
||||
load: true
|
||||
set: |
|
||||
*.cache-from=type=gha
|
||||
*.cache-to=type=gha,mode=max
|
||||
- name: Run Playwright tests
|
||||
run: docker compose up --exit-code-from playwright
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v6
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
@@ -89,25 +94,38 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.3.0
|
||||
uses: docker/login-action@v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: amir20/dozzle
|
||||
images: |
|
||||
amir20/dozzle
|
||||
ghcr.io/amir20/dozzle
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=semver,pattern={{version}},prefix=v
|
||||
type=semver,pattern={{major}}.{{minor}},prefix=v
|
||||
type=semver,pattern={{major}},prefix=v
|
||||
- name: Writing certs to file
|
||||
run: |
|
||||
echo "${{ secrets.TTL_KEY }}" > shared_key.pem
|
||||
echo "${{ secrets.TTL_CERT }}" > shared_cert.pem
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
uses: docker/build-push-action@v6.18.0
|
||||
with:
|
||||
push: true
|
||||
context: .
|
||||
@@ -123,11 +141,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
- name: Release to Github
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -13,25 +13,33 @@ jobs:
|
||||
if: ${{ !github.event.repository.fork && !github.event.pull_request.head.repo.fork && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == 'amir20/dozzle') }}
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.3.0
|
||||
uses: docker/login-action@v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: amir20/dozzle
|
||||
images: |
|
||||
amir20/dozzle
|
||||
ghcr.io/amir20/dozzle
|
||||
- name: Writing certs to file
|
||||
run: |
|
||||
echo "${{ secrets.TTL_KEY }}" > shared_key.pem
|
||||
echo "${{ secrets.TTL_CERT }}" > shared_cert.pem
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
uses: docker/build-push-action@v6.18.0
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
||||
@@ -24,14 +24,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0 # Not needed if lastUpdated is not enabled
|
||||
- uses: pnpm/action-setup@v2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: latest
|
||||
node-version: 24.13.0
|
||||
cache: pnpm # or pnpm / yarn
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
pnpm docs:build
|
||||
touch docs/.vitepress/dist/.nojekyll
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
path: docs/.vitepress/dist
|
||||
|
||||
|
||||
@@ -15,6 +15,6 @@ jobs:
|
||||
name: Validate PR title
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@v5
|
||||
- uses: amannn/action-semantic-pull-request@v6
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
+33
-25
@@ -11,16 +11,18 @@ jobs:
|
||||
name: Typecheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
- uses: actions/setup-node@v6
|
||||
name: Install Node
|
||||
with:
|
||||
node-version: latest
|
||||
node-version: 24.13.0
|
||||
- run: corepack enable
|
||||
- run: pnpm --version
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: latest
|
||||
node-version: 24.13.0
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "**/pnpm-lock.yaml"
|
||||
- name: Install dependencies
|
||||
@@ -31,16 +33,14 @@ jobs:
|
||||
name: JavaScript Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
name: Install Node
|
||||
with:
|
||||
node-version: latest
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
- run: corepack enable
|
||||
- run: pnpm --version
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: latest
|
||||
node-version: 24.13.0
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "**/pnpm-lock.yaml"
|
||||
- name: Install dependencies
|
||||
@@ -52,12 +52,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.23"
|
||||
go-version: "1.25.6"
|
||||
check-latest: true
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
@@ -73,28 +73,35 @@ jobs:
|
||||
name: Go Staticcheck
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.6"
|
||||
check-latest: true
|
||||
- name: Generate dependencies
|
||||
run: make fake_assets shared_key.pem shared_cert.pem
|
||||
- name: Stactic checker
|
||||
uses: dominikh/staticcheck-action@v1.3.1
|
||||
uses: dominikh/staticcheck-action@v1.4.0
|
||||
with:
|
||||
version: "latest"
|
||||
install-go: false
|
||||
int-test:
|
||||
name: Integration Tests
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
- uses: actions/setup-node@v6
|
||||
name: Install Node
|
||||
with:
|
||||
node-version: latest
|
||||
node-version: 24.13.0
|
||||
- run: corepack enable
|
||||
- run: pnpm --version
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: latest
|
||||
node-version: 24.13.0
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "**/pnpm-lock.yaml"
|
||||
- name: Install dependencies
|
||||
@@ -104,15 +111,16 @@ jobs:
|
||||
- name: Generate certs
|
||||
run: make shared_key.pem shared_cert.pem
|
||||
- name: Build
|
||||
uses: docker/bake-action@v5
|
||||
uses: docker/bake-action@v6
|
||||
with:
|
||||
source: .
|
||||
load: true
|
||||
set: |
|
||||
*.cache-from=type=gha
|
||||
*.cache-to=type=gha,mode=max
|
||||
- name: Run Playwright tests
|
||||
run: docker compose up --exit-code-from playwright
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v6
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
|
||||
@@ -14,3 +14,5 @@ coverage.out
|
||||
/playwright/.cache/
|
||||
*.pem
|
||||
*.csr
|
||||
tmp
|
||||
.claude
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
auto-imports.d.ts
|
||||
components.d.ts
|
||||
typed-router.d.ts
|
||||
docs/.vitepress/cache
|
||||
docs/.vitepress/dist
|
||||
dist
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
-r '\.(go)$' -R 'node_modules' -G '\*\_test.go' -s -- go run -race main.go --level debug agent
|
||||
@@ -1 +0,0 @@
|
||||
-r '\.(go)$' -R 'node_modules' -G '\*\_test.go' -s -- go run -race main.go --level debug
|
||||
Vendored
-16
@@ -1,16 +0,0 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug test",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/main.go",
|
||||
"args": ["test"]
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
-7
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"i18n-ally.localesPaths": ["locales"],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"cSpell.words": ["healthcheck", "orderedmap", "Retriable", "stdcopy", "Warnf"],
|
||||
"editor.formatOnSave": true,
|
||||
"i18n-ally.extract.autoDetect": true
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Comment Style
|
||||
|
||||
**Always use ultra-brief mode for all PR reviews and responses.**
|
||||
|
||||
Format:
|
||||
|
||||
- Critical issues only (bugs, security, blockers)
|
||||
- Brief bullet points, no lengthy explanations
|
||||
- Skip verbose sections (no "Strengths", "Summary", etc.)
|
||||
- Include file:line references when relevant
|
||||
- Maximum ~10-15 lines per response
|
||||
|
||||
## Project Overview
|
||||
|
||||
Dozzle is a lightweight, web-based Docker log viewer with real-time monitoring capabilities. It's a hybrid application with:
|
||||
|
||||
- **Backend**: Go (HTTP server, Docker API client, WebSocket streaming)
|
||||
- **Frontend**: Vue 3 (SPA with Vite, TypeScript)
|
||||
|
||||
The application supports multiple deployment modes: standalone server, Docker Swarm, and Kubernetes (k8s).
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Install Go tools (protobuf, air hot-reloader)
|
||||
make tools
|
||||
|
||||
# Generate certificates and protobuf files
|
||||
make generate
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
# Run full development environment (backend + frontend with hot reload)
|
||||
make dev
|
||||
|
||||
# Alternative: Run backend and frontend separately
|
||||
pnpm run watch:backend # Go backend with air (port 3100)
|
||||
pnpm run watch:frontend # Vite dev server (port 3100)
|
||||
|
||||
# Run in agent mode for development
|
||||
pnpm run agent:dev
|
||||
```
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
# Build frontend assets
|
||||
pnpm build
|
||||
# or
|
||||
make dist
|
||||
|
||||
# Build entire application (includes frontend build)
|
||||
make build
|
||||
|
||||
# Build Docker image
|
||||
make docker
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Run Go tests
|
||||
make test
|
||||
|
||||
# Run frontend tests (Vitest)
|
||||
pnpm test
|
||||
# Run in watch mode
|
||||
TZ=UTC pnpm test --watch
|
||||
|
||||
# Type checking
|
||||
pnpm typecheck
|
||||
```
|
||||
|
||||
### Preview & Other
|
||||
|
||||
```bash
|
||||
# Preview production build locally
|
||||
pnpm preview
|
||||
# or
|
||||
make preview
|
||||
|
||||
# Run integration tests (Playwright)
|
||||
make int
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Backend (Go)
|
||||
|
||||
The Go backend is organized into these key packages:
|
||||
|
||||
- **`internal/web/`** - HTTP server and routing layer
|
||||
- Routes defined in `routes.go` using chi router
|
||||
- WebSocket/SSE handlers for log streaming (`logs.go`)
|
||||
- Authentication middleware and token management (`auth.go`)
|
||||
- Container action handlers (`actions.go`)
|
||||
|
||||
- **`internal/docker/`** - Docker API client implementation
|
||||
- `client.go`: Main Docker client wrapper with container operations
|
||||
- `log_reader.go`: Streaming container logs
|
||||
- `stats_collector.go`: Real-time container stats collection
|
||||
|
||||
- **`internal/agent/`** - gRPC agent for multi-host support
|
||||
- Uses Protocol Buffers (protos defined in `protos/`)
|
||||
- Enables distributed log collection across Docker hosts
|
||||
|
||||
- **`internal/k8s/`** - Kubernetes client support
|
||||
- Alternative to Docker client for k8s deployments
|
||||
|
||||
- **`internal/support/`** - Support utilities
|
||||
- `cli/`: Command-line argument parsing and validation
|
||||
- `docker/`: Multi-host Docker management and Swarm support
|
||||
- `container/`: Container service abstractions
|
||||
- `web/`: Web service utilities
|
||||
|
||||
- **`internal/auth/`** - Authentication providers
|
||||
- Simple file-based auth (`simple.go`)
|
||||
- Forward proxy auth (`proxy.go`)
|
||||
- Role-based authorization (`roles.go`)
|
||||
|
||||
- **`internal/container/`** - Container domain models and interfaces
|
||||
|
||||
- **`main.go`** - Application entry point with mode switching (server/swarm/k8s)
|
||||
|
||||
### Frontend (Vue 3)
|
||||
|
||||
The frontend uses file-based routing with these conventions:
|
||||
|
||||
- **`assets/pages/`** - File-based routes (unplugin-vue-router)
|
||||
- `container/[id].vue`: Single container view
|
||||
- `merged/[ids].vue`: Multi-container merged view
|
||||
- `host/[id].vue`: Host-level logs
|
||||
- `service/[name].vue`: Swarm service logs
|
||||
- `stack/[name].vue`: Docker stack logs
|
||||
- `group/[name].vue`: Custom grouped logs
|
||||
|
||||
- **`assets/components/`** - Vue components (auto-imported)
|
||||
- `LogViewer/`: Core log viewing components
|
||||
- `SimpleLogItem.vue`: Single-line log entries
|
||||
- `ComplexLogItem.vue`: JSON/structured log entries
|
||||
- `GroupedLogItem.vue`: Multi-line grouped log entries
|
||||
- `ContainerEventLogItem.vue`: Container lifecycle events
|
||||
- `SkippedEntriesLogItem.vue`: Placeholder for skipped logs
|
||||
- `LoadMoreLogItem.vue`: Load more historical logs
|
||||
- `ContainerViewer/`: Container-specific UI
|
||||
- `common/`: Reusable UI components
|
||||
- `BarChart.vue`: Lightweight bar chart with automatic downsampling
|
||||
- `HostCard.vue`: Host overview card with metrics
|
||||
- `MetricCard.vue`: Reusable metric display component
|
||||
- `ContainerTable.vue`: Container table with historical stat visualization
|
||||
|
||||
- **`assets/stores/`** - Pinia stores (auto-imported)
|
||||
- `config.ts`: App configuration and feature flags
|
||||
- `container.ts`: Container state management
|
||||
- `hosts.ts`: Multi-host state
|
||||
- `settings.ts`: User preferences
|
||||
|
||||
- **`assets/composable/`** - Vue composables (auto-imported)
|
||||
- `eventStreams.ts`: SSE connection management
|
||||
- `historicalLogs.ts`: Historical log fetching
|
||||
- `logContext.ts`: Log filtering and search context
|
||||
- `storage.ts`: LocalStorage abstractions
|
||||
- `visible.ts`: Log filtering by visible keys for complex logs
|
||||
|
||||
- **`assets/modules/`** - Vue plugins
|
||||
- `router.ts`: Vue Router configuration
|
||||
- `pinia.ts`: Pinia store setup
|
||||
- `i18n.ts`: Internationalization
|
||||
|
||||
### Communication Flow
|
||||
|
||||
1. **Real-time Logs**: Frontend establishes SSE connections to `/api/hosts/{host}/containers/{id}/logs/stream`
|
||||
2. **Container Events**: SSE stream at `/api/events/stream` pushes container lifecycle events
|
||||
3. **Stats**: Real-time CPU/memory stats streamed via SSE alongside events
|
||||
4. **Actions**: POST to `/api/hosts/{host}/containers/{id}/actions/{action}` (start/stop/restart)
|
||||
5. **Terminal**: WebSocket connections for container attach/exec at `/api/hosts/{host}/containers/{id}/attach`
|
||||
|
||||
### Build System
|
||||
|
||||
- **Frontend**: Vite builds to `dist/` with manifest
|
||||
- **Backend**: Embeds `dist/` using Go embed directive
|
||||
- **Hot Reload**: In development, `DEV=true` disables embedded assets, `LIVE_FS=true` serves from filesystem
|
||||
- **Makefile**: Orchestrates builds and dependency generation
|
||||
|
||||
## Important Development Notes
|
||||
|
||||
### Frontend
|
||||
|
||||
- Auto-imports are configured for Vue composables, components, and Pinia stores (see `vite.config.ts`)
|
||||
- Icons use unplugin-icons with multiple icon sets (mdi, carbon, material-symbols, etc.)
|
||||
- Tailwind CSS with DaisyUI for styling
|
||||
- TypeScript definitions auto-generated in `assets/auto-imports.d.ts` and `assets/components.d.ts`
|
||||
- **Log Entry Types**: Three types of log messages supported
|
||||
- `SimpleLogEntry`: Single-line text logs (`string`)
|
||||
- `ComplexLogEntry`: Structured JSON logs (`JSONObject`)
|
||||
- `GroupedLogEntry`: Multi-line grouped logs (`string[]`)
|
||||
- **Type consistency**: Use `LogMessage` type alias instead of `string | string[] | JSONObject` for log entry messages
|
||||
- **Charts/Visualizations**: Custom lightweight implementations (no D3.js)
|
||||
- `BarChart.vue`: Self-contained bar chart with responsive downsampling
|
||||
- Downsampling algorithm: Averages data into buckets based on available screen width
|
||||
- All stat history tracked in `Container.statsHistory` (max 300 items via rolling window)
|
||||
|
||||
### Backend
|
||||
|
||||
- The application uses Go 1.25+ with module support
|
||||
- Certificate generation is required (`make generate` creates shared_key.pem and shared_cert.pem)
|
||||
- Protocol buffer generation happens via `go generate` directive in `main.go`
|
||||
- Docker client uses API version negotiation for compatibility
|
||||
|
||||
### Authentication
|
||||
|
||||
- Three modes: none, simple (file-based users.yml), forward-proxy (e.g., Authelia)
|
||||
- JWT tokens for simple auth with configurable TTL
|
||||
- User file location: `./data/users.yml` or `./data/users.yaml`
|
||||
|
||||
### Testing
|
||||
|
||||
- Go tests use standard `testing` package with testify assertions
|
||||
- Frontend uses Vitest with `@vue/test-utils`
|
||||
- Integration tests with Playwright in `e2e/`
|
||||
- Tests must run with `TZ=UTC` for consistent timestamps
|
||||
|
||||
### Container Stats & Metrics
|
||||
|
||||
- Stats are tracked using exponential moving average (EMA) with alpha=0.2
|
||||
- History stored in rolling window (300 items max) via `useSimpleRefHistory`
|
||||
- CPU metrics normalized by core count (respects `cpuLimit` or falls back to host `nCPU`)
|
||||
- Memory metrics include both percentage and absolute usage (`memoryUsage` vs `memory`)
|
||||
- Stats visualization uses adaptive downsampling for performance
|
||||
|
||||
### Container Labels
|
||||
|
||||
- `dev.dozzle.name`: Custom container display name
|
||||
- `dev.dozzle.group`: Group containers together
|
||||
- Label-based filtering throughout the application
|
||||
|
||||
### Deployment Modes
|
||||
|
||||
- **Server mode**: Single or multi-host Docker monitoring
|
||||
- **Swarm mode**: Automatic discovery of Swarm nodes via Docker API
|
||||
- **K8s mode**: Pod log monitoring in Kubernetes cluster
|
||||
- **Agent mode**: Lightweight gRPC agent for remote log collection
|
||||
+8
-6
@@ -1,5 +1,5 @@
|
||||
# Build assets
|
||||
FROM --platform=$BUILDPLATFORM node:22.10.0-alpine AS node
|
||||
FROM --platform=$BUILDPLATFORM node:24.13.0-alpine AS node
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
@@ -22,13 +22,13 @@ COPY public ./public
|
||||
# Build assets
|
||||
RUN pnpm build
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:1.23.2-alpine AS builder
|
||||
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder
|
||||
|
||||
# install gRPC dependencies
|
||||
RUN apk add --no-cache ca-certificates protoc protobuf-dev\
|
||||
&& mkdir /dozzle \
|
||||
&& go install google.golang.org/protobuf/cmd/protoc-gen-go@latest \
|
||||
&& go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||
&& mkdir /dozzle \
|
||||
&& go install google.golang.org/protobuf/cmd/protoc-gen-go@latest \
|
||||
&& go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||
|
||||
WORKDIR /dozzle
|
||||
|
||||
@@ -38,6 +38,7 @@ RUN go mod download
|
||||
|
||||
# Copy all other files
|
||||
COPY internal ./internal
|
||||
COPY types ./types
|
||||
COPY main.go ./
|
||||
COPY protos ./protos
|
||||
COPY shared_key.pem shared_cert.pem ./
|
||||
@@ -53,13 +54,14 @@ ARG TARGETOS TARGETARCH
|
||||
RUN go generate
|
||||
|
||||
# Build binary
|
||||
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags "-s -w -X github.com/amir20/dozzle/internal/support/cli.Version=$TAG" -o dozzle
|
||||
RUN GOEXPERIMENT=jsonv2 GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags "-s -w -X github.com/amir20/dozzle/internal/support/cli.Version=$TAG" -o dozzle
|
||||
|
||||
RUN mkdir /data
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=builder /data /data
|
||||
COPY --from=builder /tmp /tmp
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY --from=builder /dozzle/dozzle /dozzle
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Amir Raminfar
|
||||
Copyright (c) 2025 Amir Raminfar
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -43,11 +43,11 @@ int:
|
||||
docker compose up --build --force-recreate --exit-code-from playwright
|
||||
|
||||
shared_key.pem:
|
||||
@openssl genpkey -algorithm RSA -out shared_key.pem -pkeyopt rsa_keygen_bits:2048
|
||||
@openssl genpkey -algorithm Ed25519 -out shared_key.pem
|
||||
|
||||
shared_cert.pem: shared_key.pem
|
||||
@openssl req -new -key shared_key.pem -out shared_request.csr -subj "/C=US/ST=California/L=San Francisco/O=Dozzle"
|
||||
@openssl x509 -req -in shared_request.csr -signkey shared_key.pem -out shared_cert.pem -days 365
|
||||
@openssl x509 -req -in shared_request.csr -signkey shared_key.pem -out shared_cert.pem -days 1825
|
||||
@rm shared_request.csr
|
||||
|
||||
$(GEN_DIR)/%.pb.go: $(PROTO_DIR)/%.proto
|
||||
@@ -61,7 +61,7 @@ push: docker
|
||||
tools:
|
||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||
go install github.com/cespare/reflex@latest
|
||||
go install github.com/air-verse/air@latest
|
||||
|
||||
run: docker
|
||||
docker run -it --rm -p 8080:8080 -v /var/run/docker.sock:/var/run/docker.sock amir20/dozzle:latest
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
<p align="center">
|
||||
<img src="assets/logo.svg" alt="Dozzle Logo" width="200"/>
|
||||
</p>
|
||||
|
||||
# Dozzle - [dozzle.dev](https://dozzle.dev/)
|
||||
|
||||
Dozzle is a small lightweight application with a web based interface to monitor Docker logs. It doesn’t store any log files. It is for live monitoring of your container logs only.
|
||||
@@ -9,6 +13,9 @@ https://github.com/user-attachments/assets/66a7b4b2-d6c9-4fca-ab04-aef6cd7c0c31
|
||||
[](https://hub.docker.com/r/amir20/dozzle/)
|
||||

|
||||
|
||||
> [!NOTE]
|
||||
> If you like Dozzle, check out [`dtop`](https://github.com/amir20/dtop) which is a top like application for monitoring Docker containers. It integrates with Dozzle to allow for linking directly to container logs.
|
||||
|
||||
## Features
|
||||
|
||||
- Intelligent fuzzy search for container names 🤖
|
||||
@@ -65,7 +72,7 @@ Dozzle can be used to monitor multiple Docker hosts. You can run Dozzle in agent
|
||||
|
||||
$ docker run -v /var/run/docker.sock:/var/run/docker.sock -p 7007:7007 amir20/dozzle:latest agent
|
||||
|
||||
See the [Agent Mode](https://dozzle.dev/guide/agent-mode) documentation for more details.
|
||||
See the [Agent Mode](https://dozzle.dev/guide/agent) documentation for more details.
|
||||
|
||||
## Technical Details
|
||||
|
||||
@@ -94,9 +101,15 @@ If it's not enabled please follow [this tutorial](https://github.com/containers/
|
||||
Once you have the podman remote socket you can run Dozzle on podman.
|
||||
|
||||
```
|
||||
podman run --volume=/run/user/1000/podman/podman.sock:/var/run/docker.sock -d -p 8080:8080 amir20/dozzle:latest
|
||||
podman run --volume=/run/user/1000/podman/podman.sock:/var/run/docker.sock -d -p 8080:8080 docker.io/amir20/dozzle:latest
|
||||
```
|
||||
|
||||
Additionally you have to create a fake engine-id to prevent `host not found` errors. Podman doesn't generate an engine-id like Docker by itself due to its daemonless architecture.
|
||||
|
||||
Under `/var/lib/docker` create a file named `engine-id`. On a system with Podman you will have to create the folder path as well. Inside the file place the UUID, for instance using `uuidgen > engine-id`. After that the file should have an identifier that looks like this: `b9f1d7fc-b459-4b6e-9f7a-e3d1cd2e14a9`.
|
||||
|
||||
For more details check [Podman Infos](docs/guide/podman.md) or the [FAQ](docs/guide/faq.md#i-am-seeing-host-not-found-error-in-the-logs-how-do-i-fix-it)
|
||||
|
||||
## Security
|
||||
|
||||
Dozzle supports file based authentication and forward proxy like [Authelia](https://www.authelia.com/). These are documented at https://dozzle.dev/guide/authentication.
|
||||
@@ -109,24 +122,7 @@ If you do not want to be tracked at all, see the `--no-analytics` flag below.
|
||||
|
||||
## Environment variables and configuration
|
||||
|
||||
Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can use the CLI flags or environment variables. The table below outlines all supported options and their respective env vars.
|
||||
|
||||
| Flag | Env Variable | Default |
|
||||
| --------------------- | -------------------------- | -------------- |
|
||||
| `--addr` | `DOZZLE_ADDR` | `:8080` |
|
||||
| `--base` | `DOZZLE_BASE` | `/` |
|
||||
| `--hostname` | `DOZZLE_HOSTNAME` | `""` |
|
||||
| `--level` | `DOZZLE_LEVEL` | `info` |
|
||||
| `--auth-provider` | `DOZZLE_AUTH_PROVIDER` | `none` |
|
||||
| `--auth-header-user` | `DOZZLE_AUTH_HEADER_USER` | `Remote-User` |
|
||||
| `--auth-header-email` | `DOZZLE_AUTH_HEADER_EMAIL` | `Remote-Email` |
|
||||
| `--auth-header-name` | `DOZZLE_AUTH_HEADER_NAME` | `Remote-Name` |
|
||||
| `--enable-actions` | `DOZZLE_ENABLE_ACTIONS` | false |
|
||||
| `--filter` | `DOZZLE_FILTER` | `""` |
|
||||
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
|
||||
| `--mode` | `DOZZLE_MODE` | `server` |
|
||||
| `--remote-host` | `DOZZLE_REMOTE_HOST` | |
|
||||
| `--remote-agent` | `DOZZLE_REMOTE_AGENT` | |
|
||||
Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can use the CLI flags or environment variables. See documentation at [https://dozzle.dev/guide/supported-env-vars](https://dozzle.dev/guide/supported-env-vars) for more details.
|
||||
|
||||
## Support
|
||||
|
||||
|
||||
+2
-1
@@ -15,10 +15,11 @@ watchEffect(() => {
|
||||
if (theme === "auto") {
|
||||
theme = mode.value;
|
||||
}
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", theme == "dark" ? "#121212" : "#F5F5F5");
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
});
|
||||
</script>
|
||||
<style lang="postcss">
|
||||
<style>
|
||||
html.has-custom-scrollbars {
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
|
||||
Vendored
+419
-360
@@ -6,370 +6,410 @@
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const DEFAULT_SETTINGS: typeof import('./stores/settings')['DEFAULT_SETTINGS']
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||
const allLevels: typeof import('./composable/logContext')['allLevels']
|
||||
const arrayEquals: typeof import('./utils/index')['arrayEquals']
|
||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||
const automaticRedirect: typeof import('./stores/settings')['automaticRedirect']
|
||||
const collapseNav: typeof import('./stores/settings')['collapseNav']
|
||||
const compact: typeof import('./stores/settings')['compact']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const computedAsync: typeof import('@vueuse/core')['computedAsync']
|
||||
const computedEager: typeof import('@vueuse/core')['computedEager']
|
||||
const computedInject: typeof import('@vueuse/core')['computedInject']
|
||||
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
|
||||
const config: typeof import('./stores/config')['default']
|
||||
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
|
||||
const controlledRef: typeof import('@vueuse/core')['controlledRef']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const createDrawer: typeof import('./composable/drawer')['createDrawer']
|
||||
const createEventHook: typeof import('@vueuse/core')['createEventHook']
|
||||
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
||||
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
||||
const createPinia: typeof import('pinia')['createPinia']
|
||||
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
||||
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
|
||||
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
||||
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
|
||||
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const dateLocale: typeof import('./stores/settings')['dateLocale']
|
||||
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
|
||||
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const defineStore: typeof import('pinia')['defineStore']
|
||||
const drawerContext: typeof import('./composable/drawer')['drawerContext']
|
||||
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const extendRef: typeof import('@vueuse/core')['extendRef']
|
||||
const flattenJSON: typeof import('./utils/index')['flattenJSON']
|
||||
const flattenJSONToMap: typeof import('./utils/index')['flattenJSONToMap']
|
||||
const formatBytes: typeof import('./utils/index')['formatBytes']
|
||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const getDeep: typeof import('./utils/index')['getDeep']
|
||||
const globalShowPopup: typeof import('./composable/popup')['globalShowPopup']
|
||||
const h: typeof import('vue')['h']
|
||||
const hashCode: typeof import('./utils/index')['hashCode']
|
||||
const hourStyle: typeof import('./stores/settings')['hourStyle']
|
||||
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const injectLocal: typeof import('@vueuse/core')['injectLocal']
|
||||
const isDefined: typeof import('@vueuse/core')['isDefined']
|
||||
const isMobile: typeof import('./composable/media')['isMobile']
|
||||
const isObject: typeof import('./utils/index')['isObject']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const lightTheme: typeof import('./stores/settings')['lightTheme']
|
||||
const locale: typeof import('./stores/settings')['locale']
|
||||
const loggingContextKey: typeof import('./composable/logContext')['loggingContextKey']
|
||||
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
||||
const mapActions: typeof import('pinia')['mapActions']
|
||||
const mapGetters: typeof import('pinia')['mapGetters']
|
||||
const mapState: typeof import('pinia')['mapState']
|
||||
const mapStores: typeof import('pinia')['mapStores']
|
||||
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const menuWidth: typeof import('./stores/settings')['menuWidth']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
|
||||
const onLongPress: typeof import('@vueuse/core')['onLongPress']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
|
||||
const persistentVisibleKeysForContainer: typeof import('./composable/storage')['persistentVisibleKeysForContainer']
|
||||
const pinnedContainers: typeof import('./composable/storage')['pinnedContainers']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const provideLocal: typeof import('@vueuse/core')['provideLocal']
|
||||
const provideLoggingContext: typeof import('./composable/logContext')['provideLoggingContext']
|
||||
const provideScrollContext: typeof import('./composable/scrollContext')['provideScrollContext']
|
||||
const reactify: typeof import('@vueuse/core')['reactify']
|
||||
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
|
||||
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
|
||||
const reactivePick: typeof import('@vueuse/core')['reactivePick']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
|
||||
const refDebounced: typeof import('@vueuse/core')['refDebounced']
|
||||
const refDefault: typeof import('@vueuse/core')['refDefault']
|
||||
const refThrottled: typeof import('@vueuse/core')['refThrottled']
|
||||
const refWithControl: typeof import('@vueuse/core')['refWithControl']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
||||
const DEFAULT_SETTINGS: typeof import('./stores/settings').DEFAULT_SETTINGS
|
||||
const EffectScope: typeof import('vue').EffectScope
|
||||
const K8sNamespace: typeof import('./stores/k8s').K8sNamespace
|
||||
const K8sOwner: typeof import('./stores/k8s').K8sOwner
|
||||
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
|
||||
const allLevels: typeof import('./composable/logContext').allLevels
|
||||
const arrayEquals: typeof import('./utils/index').arrayEquals
|
||||
const asyncComputed: typeof import('@vueuse/core').asyncComputed
|
||||
const autoResetRef: typeof import('@vueuse/core').autoResetRef
|
||||
const automaticRedirect: typeof import('./stores/settings').automaticRedirect
|
||||
const collapseNav: typeof import('./stores/settings').collapseNav
|
||||
const compact: typeof import('./stores/settings').compact
|
||||
const computed: typeof import('vue').computed
|
||||
const computedAsync: typeof import('@vueuse/core').computedAsync
|
||||
const computedEager: typeof import('@vueuse/core').computedEager
|
||||
const computedInject: typeof import('@vueuse/core').computedInject
|
||||
const computedWithControl: typeof import('@vueuse/core').computedWithControl
|
||||
const config: typeof import('./stores/config').default
|
||||
const controlledComputed: typeof import('@vueuse/core').controlledComputed
|
||||
const controlledRef: typeof import('@vueuse/core').controlledRef
|
||||
const createApp: typeof import('vue').createApp
|
||||
const createDrawer: typeof import('./composable/drawer').createDrawer
|
||||
const createEventHook: typeof import('@vueuse/core').createEventHook
|
||||
const createGlobalState: typeof import('@vueuse/core').createGlobalState
|
||||
const createInjectionState: typeof import('@vueuse/core').createInjectionState
|
||||
const createPinia: typeof import('pinia').createPinia
|
||||
const createReactiveFn: typeof import('@vueuse/core').createReactiveFn
|
||||
const createRef: typeof import('@vueuse/core').createRef
|
||||
const createReusableTemplate: typeof import('@vueuse/core').createReusableTemplate
|
||||
const createSharedComposable: typeof import('@vueuse/core').createSharedComposable
|
||||
const createTemplatePromise: typeof import('@vueuse/core').createTemplatePromise
|
||||
const createUnrefFn: typeof import('@vueuse/core').createUnrefFn
|
||||
const customRef: typeof import('vue').customRef
|
||||
const dateLocale: typeof import('./stores/settings').dateLocale
|
||||
const debouncedRef: typeof import('@vueuse/core').debouncedRef
|
||||
const debouncedWatch: typeof import('@vueuse/core').debouncedWatch
|
||||
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
|
||||
const defineComponent: typeof import('vue').defineComponent
|
||||
const defineStore: typeof import('pinia').defineStore
|
||||
const drawerContext: typeof import('./composable/drawer').drawerContext
|
||||
const eagerComputed: typeof import('@vueuse/core').eagerComputed
|
||||
const effectScope: typeof import('vue').effectScope
|
||||
const extendRef: typeof import('@vueuse/core').extendRef
|
||||
const flattenJSON: typeof import('./utils/index').flattenJSON
|
||||
const flattenJSONToMap: typeof import('./utils/index').flattenJSONToMap
|
||||
const formatBytes: typeof import('./utils/index').formatBytes
|
||||
const getActivePinia: typeof import('pinia').getActivePinia
|
||||
const getCurrentInstance: typeof import('vue').getCurrentInstance
|
||||
const getCurrentScope: typeof import('vue').getCurrentScope
|
||||
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
|
||||
const getDeep: typeof import('./utils/index').getDeep
|
||||
const globalShowPopup: typeof import('./composable/popup').globalShowPopup
|
||||
const groupContainers: typeof import('./stores/settings').groupContainers
|
||||
const h: typeof import('vue').h
|
||||
const hashCode: typeof import('./utils/index').hashCode
|
||||
const hourStyle: typeof import('./stores/settings').hourStyle
|
||||
const ignorableWatch: typeof import('@vueuse/core').ignorableWatch
|
||||
const inject: typeof import('vue').inject
|
||||
const injectLocal: typeof import('@vueuse/core').injectLocal
|
||||
const isDefined: typeof import('@vueuse/core').isDefined
|
||||
const isMobile: typeof import('./composable/media').isMobile
|
||||
const isObject: typeof import('./utils/index').isObject
|
||||
const isProxy: typeof import('vue').isProxy
|
||||
const isReactive: typeof import('vue').isReactive
|
||||
const isReadonly: typeof import('vue').isReadonly
|
||||
const isRef: typeof import('vue').isRef
|
||||
const isShallow: typeof import('vue').isShallow
|
||||
const lightTheme: typeof import('./stores/settings').lightTheme
|
||||
const loadBetween: typeof import('./composable/eventStreams').loadBetween
|
||||
const locale: typeof import('./stores/settings').locale
|
||||
const loggingContextKey: typeof import('./composable/logContext').loggingContextKey
|
||||
const makeDestructurable: typeof import('@vueuse/core').makeDestructurable
|
||||
const manualResetRef: typeof import('@vueuse/core')['manualResetRef']
|
||||
const mapActions: typeof import('pinia').mapActions
|
||||
const mapGetters: typeof import('pinia').mapGetters
|
||||
const mapState: typeof import('pinia').mapState
|
||||
const mapStores: typeof import('pinia').mapStores
|
||||
const mapWritableState: typeof import('pinia').mapWritableState
|
||||
const markRaw: typeof import('vue').markRaw
|
||||
const menuWidth: typeof import('./stores/settings').menuWidth
|
||||
const nextTick: typeof import('vue').nextTick
|
||||
const onActivated: typeof import('vue').onActivated
|
||||
const onBeforeMount: typeof import('vue').onBeforeMount
|
||||
const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
|
||||
const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
|
||||
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
|
||||
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
|
||||
const onClickOutside: typeof import('@vueuse/core').onClickOutside
|
||||
const onDeactivated: typeof import('vue').onDeactivated
|
||||
const onElementRemoval: typeof import('@vueuse/core').onElementRemoval
|
||||
const onErrorCaptured: typeof import('vue').onErrorCaptured
|
||||
const onKeyStroke: typeof import('@vueuse/core').onKeyStroke
|
||||
const onLongPress: typeof import('@vueuse/core').onLongPress
|
||||
const onMounted: typeof import('vue').onMounted
|
||||
const onRenderTracked: typeof import('vue').onRenderTracked
|
||||
const onRenderTriggered: typeof import('vue').onRenderTriggered
|
||||
const onScopeDispose: typeof import('vue').onScopeDispose
|
||||
const onServerPrefetch: typeof import('vue').onServerPrefetch
|
||||
const onStartTyping: typeof import('@vueuse/core').onStartTyping
|
||||
const onUnmounted: typeof import('vue').onUnmounted
|
||||
const onUpdated: typeof import('vue').onUpdated
|
||||
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
|
||||
const pausableWatch: typeof import('@vueuse/core').pausableWatch
|
||||
const persistentVisibleKeysForContainer: typeof import('./composable/storage').persistentVisibleKeysForContainer
|
||||
const pinnedContainers: typeof import('./composable/storage').pinnedContainers
|
||||
const provide: typeof import('vue').provide
|
||||
const provideLocal: typeof import('@vueuse/core').provideLocal
|
||||
const provideLoggingContext: typeof import('./composable/logContext').provideLoggingContext
|
||||
const provideScrollContext: typeof import('./composable/scrollContext').provideScrollContext
|
||||
const reactify: typeof import('@vueuse/core').reactify
|
||||
const reactifyObject: typeof import('@vueuse/core').reactifyObject
|
||||
const reactive: typeof import('vue').reactive
|
||||
const reactiveComputed: typeof import('@vueuse/core').reactiveComputed
|
||||
const reactiveOmit: typeof import('@vueuse/core').reactiveOmit
|
||||
const reactivePick: typeof import('@vueuse/core').reactivePick
|
||||
const readonly: typeof import('vue').readonly
|
||||
const ref: typeof import('vue').ref
|
||||
const refAutoReset: typeof import('@vueuse/core').refAutoReset
|
||||
const refDebounced: typeof import('@vueuse/core').refDebounced
|
||||
const refDefault: typeof import('@vueuse/core').refDefault
|
||||
const refManualReset: typeof import('@vueuse/core').refManualReset
|
||||
const refThrottled: typeof import('@vueuse/core').refThrottled
|
||||
const refWithControl: typeof import('@vueuse/core').refWithControl
|
||||
const resolveComponent: typeof import('vue').resolveComponent
|
||||
const resolveRef: typeof import('@vueuse/core').resolveRef
|
||||
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
||||
const scrollContextKey: typeof import('./composable/scrollContext')['scrollContextKey']
|
||||
const search: typeof import('./stores/settings')['search']
|
||||
const sessionHost: typeof import('./composable/storage')['sessionHost']
|
||||
const setActivePinia: typeof import('pinia')['setActivePinia']
|
||||
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
||||
const setTitle: typeof import('./composable/title')['setTitle']
|
||||
const settings: typeof import('./stores/settings')['settings']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const showAllContainers: typeof import('./stores/settings')['showAllContainers']
|
||||
const showStd: typeof import('./stores/settings')['showStd']
|
||||
const showTimestamp: typeof import('./stores/settings')['showTimestamp']
|
||||
const size: typeof import('./stores/settings')['size']
|
||||
const smallerScrollbars: typeof import('./stores/settings')['smallerScrollbars']
|
||||
const softWrap: typeof import('./stores/settings')['softWrap']
|
||||
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||
const stripVersion: typeof import('./utils/index')['stripVersion']
|
||||
const syncRef: typeof import('@vueuse/core')['syncRef']
|
||||
const syncRefs: typeof import('@vueuse/core')['syncRefs']
|
||||
const templateRef: typeof import('@vueuse/core')['templateRef']
|
||||
const throttledRef: typeof import('@vueuse/core')['throttledRef']
|
||||
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toReactive: typeof import('@vueuse/core')['toReactive']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
|
||||
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
|
||||
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
|
||||
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
|
||||
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const unrefElement: typeof import('@vueuse/core')['unrefElement']
|
||||
const until: typeof import('@vueuse/core')['until']
|
||||
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
||||
const useAnimate: typeof import('@vueuse/core')['useAnimate']
|
||||
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
|
||||
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
|
||||
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
|
||||
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
|
||||
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
|
||||
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
|
||||
const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
|
||||
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
|
||||
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
|
||||
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
|
||||
const useArraySome: typeof import('@vueuse/core')['useArraySome']
|
||||
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
|
||||
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
||||
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useBase64: typeof import('@vueuse/core')['useBase64']
|
||||
const useBattery: typeof import('@vueuse/core')['useBattery']
|
||||
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
|
||||
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
|
||||
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
|
||||
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
|
||||
const useCached: typeof import('@vueuse/core')['useCached']
|
||||
const useClipboard: typeof import('@vueuse/core')['useClipboard']
|
||||
const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
|
||||
const useCloned: typeof import('@vueuse/core')['useCloned']
|
||||
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
||||
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
||||
const useContainerActions: typeof import('./composable/containerActions')['useContainerActions']
|
||||
const useContainerStore: typeof import('./stores/container')['useContainerStore']
|
||||
const useContainerStream: typeof import('./composable/eventStreams')['useContainerStream']
|
||||
const useCounter: typeof import('@vueuse/core')['useCounter']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVar: typeof import('@vueuse/core')['useCssVar']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
|
||||
const useCycleList: typeof import('@vueuse/core')['useCycleList']
|
||||
const useDark: typeof import('@vueuse/core')['useDark']
|
||||
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
|
||||
const useDebounce: typeof import('@vueuse/core')['useDebounce']
|
||||
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
|
||||
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
|
||||
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
|
||||
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
|
||||
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
|
||||
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
|
||||
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
|
||||
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
|
||||
const useDraggable: typeof import('@vueuse/core')['useDraggable']
|
||||
const useDrawer: typeof import('./composable/drawer')['useDrawer']
|
||||
const useDropZone: typeof import('@vueuse/core')['useDropZone']
|
||||
const useDuckDB: typeof import('./composable/duckdb')['useDuckDB']
|
||||
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
|
||||
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
|
||||
const useElementHover: typeof import('@vueuse/core')['useElementHover']
|
||||
const useElementSize: typeof import('@vueuse/core')['useElementSize']
|
||||
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
|
||||
const useEventBus: typeof import('@vueuse/core')['useEventBus']
|
||||
const useEventListener: typeof import('@vueuse/core')['useEventListener']
|
||||
const useEventSource: typeof import('@vueuse/core')['useEventSource']
|
||||
const useExponentialMovingAverage: typeof import('./utils/index')['useExponentialMovingAverage']
|
||||
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
|
||||
const useFavicon: typeof import('@vueuse/core')['useFavicon']
|
||||
const useFetch: typeof import('@vueuse/core')['useFetch']
|
||||
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
|
||||
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
|
||||
const useFocus: typeof import('@vueuse/core')['useFocus']
|
||||
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
|
||||
const useFps: typeof import('@vueuse/core')['useFps']
|
||||
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
|
||||
const useGamepad: typeof import('@vueuse/core')['useGamepad']
|
||||
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
|
||||
const useGroupedStream: typeof import('./composable/eventStreams')['useGroupedStream']
|
||||
const useHead: typeof import('@vueuse/head')['useHead']
|
||||
const useHosts: typeof import('./stores/hosts')['useHosts']
|
||||
const useI18n: typeof import('vue-i18n')['useI18n']
|
||||
const useId: typeof import('vue')['useId']
|
||||
const useIdle: typeof import('@vueuse/core')['useIdle']
|
||||
const useImage: typeof import('@vueuse/core')['useImage']
|
||||
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
|
||||
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
|
||||
const useInterval: typeof import('@vueuse/core')['useInterval']
|
||||
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
|
||||
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
|
||||
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
|
||||
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
|
||||
const useLoggingContext: typeof import('./composable/logContext')['useLoggingContext']
|
||||
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
|
||||
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
|
||||
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
|
||||
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
|
||||
const useMemoize: typeof import('@vueuse/core')['useMemoize']
|
||||
const useMemory: typeof import('@vueuse/core')['useMemory']
|
||||
const useMergedStream: typeof import('./composable/eventStreams')['useMergedStream']
|
||||
const useModel: typeof import('vue')['useModel']
|
||||
const useMounted: typeof import('@vueuse/core')['useMounted']
|
||||
const useMouse: typeof import('@vueuse/core')['useMouse']
|
||||
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
|
||||
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
|
||||
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
|
||||
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
|
||||
const useNetwork: typeof import('@vueuse/core')['useNetwork']
|
||||
const useNow: typeof import('@vueuse/core')['useNow']
|
||||
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
|
||||
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
|
||||
const useOnline: typeof import('@vueuse/core')['useOnline']
|
||||
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
|
||||
const useParallax: typeof import('@vueuse/core')['useParallax']
|
||||
const useParentElement: typeof import('@vueuse/core')['useParentElement']
|
||||
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
|
||||
const usePermission: typeof import('@vueuse/core')['usePermission']
|
||||
const usePinnedLogsStore: typeof import('./stores/pinned')['usePinnedLogsStore']
|
||||
const usePointer: typeof import('@vueuse/core')['usePointer']
|
||||
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
|
||||
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
|
||||
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
|
||||
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
|
||||
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
||||
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
||||
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
|
||||
const usePrevious: typeof import('@vueuse/core')['usePrevious']
|
||||
const useProfileStorage: typeof import('./composable/profileStorage')['useProfileStorage']
|
||||
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
||||
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||
const useReleases: typeof import('./stores/releases')['useReleases']
|
||||
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
|
||||
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
|
||||
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
||||
const useScroll: typeof import('@vueuse/core')['useScroll']
|
||||
const useScrollContext: typeof import('./composable/scrollContext')['useScrollContext']
|
||||
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
|
||||
const useSearchFilter: typeof import('./composable/search')['useSearchFilter']
|
||||
const useSeoMeta: typeof import('@vueuse/head')['useSeoMeta']
|
||||
const useServiceStream: typeof import('./composable/eventStreams')['useServiceStream']
|
||||
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
||||
const useShare: typeof import('@vueuse/core')['useShare']
|
||||
const useSimpleRefHistory: typeof import('./utils/index')['useSimpleRefHistory']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useSorted: typeof import('@vueuse/core')['useSorted']
|
||||
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
||||
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
|
||||
const useStackStream: typeof import('./composable/eventStreams')['useStackStream']
|
||||
const useStepper: typeof import('@vueuse/core')['useStepper']
|
||||
const useStorage: typeof import('@vueuse/core')['useStorage']
|
||||
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
|
||||
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
|
||||
const useSupported: typeof import('@vueuse/core')['useSupported']
|
||||
const useSwarmStore: typeof import('./stores/swarm')['useSwarmStore']
|
||||
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
||||
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
||||
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
|
||||
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
|
||||
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
|
||||
const useThrottle: typeof import('@vueuse/core')['useThrottle']
|
||||
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
|
||||
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
|
||||
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
|
||||
const useTimeout: typeof import('@vueuse/core')['useTimeout']
|
||||
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
|
||||
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
|
||||
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
|
||||
const useTitle: typeof import('@vueuse/core')['useTitle']
|
||||
const useToNumber: typeof import('@vueuse/core')['useToNumber']
|
||||
const useToString: typeof import('@vueuse/core')['useToString']
|
||||
const useToast: typeof import('./composable/toast')['useToast']
|
||||
const useToggle: typeof import('@vueuse/core')['useToggle']
|
||||
const useTransition: typeof import('@vueuse/core')['useTransition']
|
||||
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
||||
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
|
||||
const useVModel: typeof import('@vueuse/core')['useVModel']
|
||||
const useVModels: typeof import('@vueuse/core')['useVModels']
|
||||
const useVibrate: typeof import('@vueuse/core')['useVibrate']
|
||||
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
|
||||
const useVisibleFilter: typeof import('./composable/visible')['useVisibleFilter']
|
||||
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
|
||||
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
|
||||
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
|
||||
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
|
||||
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
|
||||
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
|
||||
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
|
||||
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchArray: typeof import('@vueuse/core')['watchArray']
|
||||
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
||||
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
|
||||
const watchDeep: typeof import('@vueuse/core')['watchDeep']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
|
||||
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
|
||||
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
||||
const watchPausable: typeof import('@vueuse/core')['watchPausable']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
|
||||
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
|
||||
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
|
||||
const whenever: typeof import('@vueuse/core')['whenever']
|
||||
const withBase: typeof import('./stores/config')['withBase']
|
||||
const scrollContextKey: typeof import('./composable/scrollContext').scrollContextKey
|
||||
const search: typeof import('./stores/settings').search
|
||||
const sessionHost: typeof import('./composable/storage').sessionHost
|
||||
const setActivePinia: typeof import('pinia').setActivePinia
|
||||
const setMapStoreSuffix: typeof import('pinia').setMapStoreSuffix
|
||||
const setTitle: typeof import('./composable/title').setTitle
|
||||
const settings: typeof import('./stores/settings').settings
|
||||
const shallowReactive: typeof import('vue').shallowReactive
|
||||
const shallowReadonly: typeof import('vue').shallowReadonly
|
||||
const shallowRef: typeof import('vue').shallowRef
|
||||
const showAllContainers: typeof import('./stores/settings').showAllContainers
|
||||
const showStd: typeof import('./stores/settings').showStd
|
||||
const showTimestamp: typeof import('./stores/settings').showTimestamp
|
||||
const size: typeof import('./stores/settings').size
|
||||
const smallerScrollbars: typeof import('./stores/settings').smallerScrollbars
|
||||
const softWrap: typeof import('./stores/settings').softWrap
|
||||
const storeToRefs: typeof import('pinia').storeToRefs
|
||||
const stripVersion: typeof import('./utils/index').stripVersion
|
||||
const syncRef: typeof import('@vueuse/core').syncRef
|
||||
const syncRefs: typeof import('@vueuse/core').syncRefs
|
||||
const templateRef: typeof import('@vueuse/core').templateRef
|
||||
const throttledRef: typeof import('@vueuse/core').throttledRef
|
||||
const throttledWatch: typeof import('@vueuse/core').throttledWatch
|
||||
const toRaw: typeof import('vue').toRaw
|
||||
const toReactive: typeof import('@vueuse/core').toReactive
|
||||
const toRef: typeof import('vue').toRef
|
||||
const toRefs: typeof import('vue').toRefs
|
||||
const toRelativeTime: typeof import('./utils/index').toRelativeTime
|
||||
const toValue: typeof import('vue').toValue
|
||||
const triggerRef: typeof import('vue').triggerRef
|
||||
const tryOnBeforeMount: typeof import('@vueuse/core').tryOnBeforeMount
|
||||
const tryOnBeforeUnmount: typeof import('@vueuse/core').tryOnBeforeUnmount
|
||||
const tryOnMounted: typeof import('@vueuse/core').tryOnMounted
|
||||
const tryOnScopeDispose: typeof import('@vueuse/core').tryOnScopeDispose
|
||||
const tryOnUnmounted: typeof import('@vueuse/core').tryOnUnmounted
|
||||
const unref: typeof import('vue').unref
|
||||
const unrefElement: typeof import('@vueuse/core').unrefElement
|
||||
const until: typeof import('@vueuse/core').until
|
||||
const useActiveElement: typeof import('@vueuse/core').useActiveElement
|
||||
const useAnimate: typeof import('@vueuse/core').useAnimate
|
||||
const useAnnouncements: typeof import('./stores/announcements').useAnnouncements
|
||||
const useArrayDifference: typeof import('@vueuse/core').useArrayDifference
|
||||
const useArrayEvery: typeof import('@vueuse/core').useArrayEvery
|
||||
const useArrayFilter: typeof import('@vueuse/core').useArrayFilter
|
||||
const useArrayFind: typeof import('@vueuse/core').useArrayFind
|
||||
const useArrayFindIndex: typeof import('@vueuse/core').useArrayFindIndex
|
||||
const useArrayFindLast: typeof import('@vueuse/core').useArrayFindLast
|
||||
const useArrayIncludes: typeof import('@vueuse/core').useArrayIncludes
|
||||
const useArrayJoin: typeof import('@vueuse/core').useArrayJoin
|
||||
const useArrayMap: typeof import('@vueuse/core').useArrayMap
|
||||
const useArrayReduce: typeof import('@vueuse/core').useArrayReduce
|
||||
const useArraySome: typeof import('@vueuse/core').useArraySome
|
||||
const useArrayUnique: typeof import('@vueuse/core').useArrayUnique
|
||||
const useAsyncQueue: typeof import('@vueuse/core').useAsyncQueue
|
||||
const useAsyncState: typeof import('@vueuse/core').useAsyncState
|
||||
const useAttrs: typeof import('vue').useAttrs
|
||||
const useBase64: typeof import('@vueuse/core').useBase64
|
||||
const useBattery: typeof import('@vueuse/core').useBattery
|
||||
const useBluetooth: typeof import('@vueuse/core').useBluetooth
|
||||
const useBreakpoints: typeof import('@vueuse/core').useBreakpoints
|
||||
const useBroadcastChannel: typeof import('@vueuse/core').useBroadcastChannel
|
||||
const useBrowserLocation: typeof import('@vueuse/core').useBrowserLocation
|
||||
const useCached: typeof import('@vueuse/core').useCached
|
||||
const useClipboard: typeof import('@vueuse/core').useClipboard
|
||||
const useClipboardItems: typeof import('@vueuse/core').useClipboardItems
|
||||
const useCloned: typeof import('@vueuse/core').useCloned
|
||||
const useColorMode: typeof import('@vueuse/core').useColorMode
|
||||
const useConfirmDialog: typeof import('@vueuse/core').useConfirmDialog
|
||||
const useContainerActions: typeof import('./composable/containerActions').useContainerActions
|
||||
const useContainerStore: typeof import('./stores/container').useContainerStore
|
||||
const useContainerStream: typeof import('./composable/eventStreams').useContainerStream
|
||||
const useCountdown: typeof import('@vueuse/core').useCountdown
|
||||
const useCounter: typeof import('@vueuse/core').useCounter
|
||||
const useCssModule: typeof import('vue').useCssModule
|
||||
const useCssVar: typeof import('@vueuse/core').useCssVar
|
||||
const useCssVars: typeof import('vue').useCssVars
|
||||
const useCurrentElement: typeof import('@vueuse/core').useCurrentElement
|
||||
const useCycleList: typeof import('@vueuse/core').useCycleList
|
||||
const useDark: typeof import('@vueuse/core').useDark
|
||||
const useDateFormat: typeof import('@vueuse/core').useDateFormat
|
||||
const useDebounce: typeof import('@vueuse/core').useDebounce
|
||||
const useDebounceFn: typeof import('@vueuse/core').useDebounceFn
|
||||
const useDebouncedRefHistory: typeof import('@vueuse/core').useDebouncedRefHistory
|
||||
const useDeviceMotion: typeof import('@vueuse/core').useDeviceMotion
|
||||
const useDeviceOrientation: typeof import('@vueuse/core').useDeviceOrientation
|
||||
const useDevicePixelRatio: typeof import('@vueuse/core').useDevicePixelRatio
|
||||
const useDevicesList: typeof import('@vueuse/core').useDevicesList
|
||||
const useDisplayMedia: typeof import('@vueuse/core').useDisplayMedia
|
||||
const useDocumentVisibility: typeof import('@vueuse/core').useDocumentVisibility
|
||||
const useDownloadUrl: typeof import('./composable/downloadUrl').useDownloadUrl
|
||||
const useDraggable: typeof import('@vueuse/core').useDraggable
|
||||
const useDrawer: typeof import('./composable/drawer').useDrawer
|
||||
const useDropZone: typeof import('@vueuse/core').useDropZone
|
||||
const useDuckDB: typeof import('./composable/duckdb').useDuckDB
|
||||
const useElementBounding: typeof import('@vueuse/core').useElementBounding
|
||||
const useElementByPoint: typeof import('@vueuse/core').useElementByPoint
|
||||
const useElementHover: typeof import('@vueuse/core').useElementHover
|
||||
const useElementSize: typeof import('@vueuse/core').useElementSize
|
||||
const useElementVisibility: typeof import('@vueuse/core').useElementVisibility
|
||||
const useEventBus: typeof import('@vueuse/core').useEventBus
|
||||
const useEventListener: typeof import('@vueuse/core').useEventListener
|
||||
const useEventSource: typeof import('@vueuse/core').useEventSource
|
||||
const useExponentialMovingAverage: typeof import('./utils/index').useExponentialMovingAverage
|
||||
const useEyeDropper: typeof import('@vueuse/core').useEyeDropper
|
||||
const useFavicon: typeof import('@vueuse/core').useFavicon
|
||||
const useFetch: typeof import('@vueuse/core').useFetch
|
||||
const useFileDialog: typeof import('@vueuse/core').useFileDialog
|
||||
const useFileSystemAccess: typeof import('@vueuse/core').useFileSystemAccess
|
||||
const useFocus: typeof import('@vueuse/core').useFocus
|
||||
const useFocusWithin: typeof import('@vueuse/core').useFocusWithin
|
||||
const useFps: typeof import('@vueuse/core').useFps
|
||||
const useFullscreen: typeof import('@vueuse/core').useFullscreen
|
||||
const useGamepad: typeof import('@vueuse/core').useGamepad
|
||||
const useGeolocation: typeof import('@vueuse/core').useGeolocation
|
||||
const useGroupedStream: typeof import('./composable/eventStreams').useGroupedStream
|
||||
const useHead: typeof import('@vueuse/head').useHead
|
||||
const useHistoricalContainerLog: typeof import('./composable/historicalLogs').useHistoricalContainerLog
|
||||
const useHostStream: typeof import('./composable/eventStreams').useHostStream
|
||||
const useHosts: typeof import('./stores/hosts').useHosts
|
||||
const useI18n: typeof import('vue-i18n').useI18n
|
||||
const useId: typeof import('vue').useId
|
||||
const useIdle: typeof import('@vueuse/core').useIdle
|
||||
const useImage: typeof import('@vueuse/core').useImage
|
||||
const useInfiniteScroll: typeof import('@vueuse/core').useInfiniteScroll
|
||||
const useIntersectionObserver: typeof import('@vueuse/core').useIntersectionObserver
|
||||
const useInterval: typeof import('@vueuse/core').useInterval
|
||||
const useIntervalFn: typeof import('@vueuse/core').useIntervalFn
|
||||
const useK8sStore: typeof import('./stores/k8s').useK8sStore
|
||||
const useKeyModifier: typeof import('@vueuse/core').useKeyModifier
|
||||
const useLastChanged: typeof import('@vueuse/core').useLastChanged
|
||||
const useLocalStorage: typeof import('@vueuse/core').useLocalStorage
|
||||
const useLoggingContext: typeof import('./composable/logContext').useLoggingContext
|
||||
const useMagicKeys: typeof import('@vueuse/core').useMagicKeys
|
||||
const useManualRefHistory: typeof import('@vueuse/core').useManualRefHistory
|
||||
const useMediaControls: typeof import('@vueuse/core').useMediaControls
|
||||
const useMediaQuery: typeof import('@vueuse/core').useMediaQuery
|
||||
const useMemoize: typeof import('@vueuse/core').useMemoize
|
||||
const useMemory: typeof import('@vueuse/core').useMemory
|
||||
const useMergedStream: typeof import('./composable/eventStreams').useMergedStream
|
||||
const useModel: typeof import('vue').useModel
|
||||
const useMounted: typeof import('@vueuse/core').useMounted
|
||||
const useMouse: typeof import('@vueuse/core').useMouse
|
||||
const useMouseInElement: typeof import('@vueuse/core').useMouseInElement
|
||||
const useMousePressed: typeof import('@vueuse/core').useMousePressed
|
||||
const useMutationObserver: typeof import('@vueuse/core').useMutationObserver
|
||||
const useNamespaceStream: typeof import('./composable/eventStreams').useNamespaceStream
|
||||
const useNavigatorLanguage: typeof import('@vueuse/core').useNavigatorLanguage
|
||||
const useNetwork: typeof import('@vueuse/core').useNetwork
|
||||
const useNow: typeof import('@vueuse/core').useNow
|
||||
const useObjectUrl: typeof import('@vueuse/core').useObjectUrl
|
||||
const useOffsetPagination: typeof import('@vueuse/core').useOffsetPagination
|
||||
const useOnline: typeof import('@vueuse/core').useOnline
|
||||
const useOwnerStream: typeof import('./composable/eventStreams').useOwnerStream
|
||||
const usePageLeave: typeof import('@vueuse/core').usePageLeave
|
||||
const useParallax: typeof import('@vueuse/core').useParallax
|
||||
const useParentElement: typeof import('@vueuse/core').useParentElement
|
||||
const usePerformanceObserver: typeof import('@vueuse/core').usePerformanceObserver
|
||||
const usePermission: typeof import('@vueuse/core').usePermission
|
||||
const usePinnedLogsStore: typeof import('./stores/pinned').usePinnedLogsStore
|
||||
const usePointer: typeof import('@vueuse/core').usePointer
|
||||
const usePointerLock: typeof import('@vueuse/core').usePointerLock
|
||||
const usePointerSwipe: typeof import('@vueuse/core').usePointerSwipe
|
||||
const usePreferredColorScheme: typeof import('@vueuse/core').usePreferredColorScheme
|
||||
const usePreferredContrast: typeof import('@vueuse/core').usePreferredContrast
|
||||
const usePreferredDark: typeof import('@vueuse/core').usePreferredDark
|
||||
const usePreferredLanguages: typeof import('@vueuse/core').usePreferredLanguages
|
||||
const usePreferredReducedMotion: typeof import('@vueuse/core').usePreferredReducedMotion
|
||||
const usePreferredReducedTransparency: typeof import('@vueuse/core').usePreferredReducedTransparency
|
||||
const usePrevious: typeof import('@vueuse/core').usePrevious
|
||||
const useProfileStorage: typeof import('./composable/profileStorage').useProfileStorage
|
||||
const useRafFn: typeof import('@vueuse/core').useRafFn
|
||||
const useRefHistory: typeof import('@vueuse/core').useRefHistory
|
||||
const useResizeObserver: typeof import('@vueuse/core').useResizeObserver
|
||||
const useRoute: typeof import('vue-router').useRoute
|
||||
const useRouter: typeof import('vue-router').useRouter
|
||||
const useSSRWidth: typeof import('@vueuse/core').useSSRWidth
|
||||
const useScreenOrientation: typeof import('@vueuse/core').useScreenOrientation
|
||||
const useScreenSafeArea: typeof import('@vueuse/core').useScreenSafeArea
|
||||
const useScriptTag: typeof import('@vueuse/core').useScriptTag
|
||||
const useScroll: typeof import('@vueuse/core').useScroll
|
||||
const useScrollContext: typeof import('./composable/scrollContext').useScrollContext
|
||||
const useScrollLock: typeof import('@vueuse/core').useScrollLock
|
||||
const useSearchFilter: typeof import('./composable/search').useSearchFilter
|
||||
const useSeoMeta: typeof import('@vueuse/head').useSeoMeta
|
||||
const useServiceStream: typeof import('./composable/eventStreams').useServiceStream
|
||||
const useSessionStorage: typeof import('@vueuse/core').useSessionStorage
|
||||
const useShare: typeof import('@vueuse/core').useShare
|
||||
const useSimpleRefHistory: typeof import('./utils/index').useSimpleRefHistory
|
||||
const useSlots: typeof import('vue').useSlots
|
||||
const useSorted: typeof import('@vueuse/core').useSorted
|
||||
const useSpeechRecognition: typeof import('@vueuse/core').useSpeechRecognition
|
||||
const useSpeechSynthesis: typeof import('@vueuse/core').useSpeechSynthesis
|
||||
const useStackStream: typeof import('./composable/eventStreams').useStackStream
|
||||
const useStepper: typeof import('@vueuse/core').useStepper
|
||||
const useStorage: typeof import('@vueuse/core').useStorage
|
||||
const useStorageAsync: typeof import('@vueuse/core').useStorageAsync
|
||||
const useStyleTag: typeof import('@vueuse/core').useStyleTag
|
||||
const useSupported: typeof import('@vueuse/core').useSupported
|
||||
const useSwarmStore: typeof import('./stores/swarm').useSwarmStore
|
||||
const useSwipe: typeof import('@vueuse/core').useSwipe
|
||||
const useTemplateRef: typeof import('vue').useTemplateRef
|
||||
const useTemplateRefsList: typeof import('@vueuse/core').useTemplateRefsList
|
||||
const useTextDirection: typeof import('@vueuse/core').useTextDirection
|
||||
const useTextSelection: typeof import('@vueuse/core').useTextSelection
|
||||
const useTextareaAutosize: typeof import('@vueuse/core').useTextareaAutosize
|
||||
const useThrottle: typeof import('@vueuse/core').useThrottle
|
||||
const useThrottleFn: typeof import('@vueuse/core').useThrottleFn
|
||||
const useThrottledRefHistory: typeof import('@vueuse/core').useThrottledRefHistory
|
||||
const useTimeAgo: typeof import('@vueuse/core').useTimeAgo
|
||||
const useTimeAgoIntl: typeof import('@vueuse/core').useTimeAgoIntl
|
||||
const useTimeout: typeof import('@vueuse/core').useTimeout
|
||||
const useTimeoutFn: typeof import('@vueuse/core').useTimeoutFn
|
||||
const useTimeoutPoll: typeof import('@vueuse/core').useTimeoutPoll
|
||||
const useTimestamp: typeof import('@vueuse/core').useTimestamp
|
||||
const useTitle: typeof import('@vueuse/core').useTitle
|
||||
const useToNumber: typeof import('@vueuse/core').useToNumber
|
||||
const useToString: typeof import('@vueuse/core').useToString
|
||||
const useToast: typeof import('./composable/toast').useToast
|
||||
const useToggle: typeof import('@vueuse/core').useToggle
|
||||
const useTransition: typeof import('@vueuse/core').useTransition
|
||||
const useUrlSearchParams: typeof import('@vueuse/core').useUrlSearchParams
|
||||
const useUserMedia: typeof import('@vueuse/core').useUserMedia
|
||||
const useVModel: typeof import('@vueuse/core').useVModel
|
||||
const useVModels: typeof import('@vueuse/core').useVModels
|
||||
const useVibrate: typeof import('@vueuse/core').useVibrate
|
||||
const useVirtualList: typeof import('@vueuse/core').useVirtualList
|
||||
const useVisibleFilter: typeof import('./composable/visible').useVisibleFilter
|
||||
const useWakeLock: typeof import('@vueuse/core').useWakeLock
|
||||
const useWebNotification: typeof import('@vueuse/core').useWebNotification
|
||||
const useWebSocket: typeof import('@vueuse/core').useWebSocket
|
||||
const useWebWorker: typeof import('@vueuse/core').useWebWorker
|
||||
const useWebWorkerFn: typeof import('@vueuse/core').useWebWorkerFn
|
||||
const useWindowFocus: typeof import('@vueuse/core').useWindowFocus
|
||||
const useWindowScroll: typeof import('@vueuse/core').useWindowScroll
|
||||
const useWindowSize: typeof import('@vueuse/core').useWindowSize
|
||||
const watch: typeof import('vue').watch
|
||||
const watchArray: typeof import('@vueuse/core').watchArray
|
||||
const watchAtMost: typeof import('@vueuse/core').watchAtMost
|
||||
const watchDebounced: typeof import('@vueuse/core').watchDebounced
|
||||
const watchDeep: typeof import('@vueuse/core').watchDeep
|
||||
const watchEffect: typeof import('vue').watchEffect
|
||||
const watchIgnorable: typeof import('@vueuse/core').watchIgnorable
|
||||
const watchImmediate: typeof import('@vueuse/core').watchImmediate
|
||||
const watchOnce: typeof import('@vueuse/core').watchOnce
|
||||
const watchPausable: typeof import('@vueuse/core').watchPausable
|
||||
const watchPostEffect: typeof import('vue').watchPostEffect
|
||||
const watchSyncEffect: typeof import('vue').watchSyncEffect
|
||||
const watchThrottled: typeof import('@vueuse/core').watchThrottled
|
||||
const watchTriggerable: typeof import('@vueuse/core').watchTriggerable
|
||||
const watchWithFilter: typeof import('@vueuse/core').watchWithFilter
|
||||
const whenever: typeof import('@vueuse/core').whenever
|
||||
const withBase: typeof import('./stores/config').withBase
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
|
||||
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
// @ts-ignore
|
||||
export type { DrawerWidth } from './composable/drawer'
|
||||
import('./composable/drawer')
|
||||
// @ts-ignore
|
||||
export type { LogStreamSource } from './composable/eventStreams'
|
||||
import('./composable/eventStreams')
|
||||
// @ts-ignore
|
||||
export type { Config, Profile } from './stores/config'
|
||||
import('./stores/config')
|
||||
// @ts-ignore
|
||||
export type { Host } from './stores/hosts'
|
||||
import('./stores/hosts')
|
||||
// @ts-ignore
|
||||
export type { K8sNamespace, K8sOwner } from './stores/k8s'
|
||||
import('./stores/k8s')
|
||||
// @ts-ignore
|
||||
export type { Settings } from './stores/settings'
|
||||
import('./stores/settings')
|
||||
}
|
||||
|
||||
// for vue template auto import
|
||||
import { UnwrapRef } from 'vue'
|
||||
declare module 'vue' {
|
||||
@@ -377,6 +417,8 @@ declare module 'vue' {
|
||||
interface ComponentCustomProperties {
|
||||
readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./stores/settings')['DEFAULT_SETTINGS']>
|
||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||
readonly K8sNamespace: UnwrapRef<typeof import('./stores/k8s')['K8sNamespace']>
|
||||
readonly K8sOwner: UnwrapRef<typeof import('./stores/k8s')['K8sOwner']>
|
||||
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
|
||||
readonly allLevels: UnwrapRef<typeof import('./composable/logContext')['allLevels']>
|
||||
readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']>
|
||||
@@ -400,6 +442,7 @@ declare module 'vue' {
|
||||
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
|
||||
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
|
||||
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
|
||||
readonly createRef: UnwrapRef<typeof import('@vueuse/core')['createRef']>
|
||||
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
|
||||
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
|
||||
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
|
||||
@@ -421,8 +464,10 @@ declare module 'vue' {
|
||||
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
|
||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||
readonly getCurrentWatcher: UnwrapRef<typeof import('vue')['getCurrentWatcher']>
|
||||
readonly getDeep: UnwrapRef<typeof import('./utils/index')['getDeep']>
|
||||
readonly globalShowPopup: UnwrapRef<typeof import('./composable/popup')['globalShowPopup']>
|
||||
readonly groupContainers: UnwrapRef<typeof import('./stores/settings')['groupContainers']>
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly hashCode: UnwrapRef<typeof import('./utils/index')['hashCode']>
|
||||
readonly hourStyle: UnwrapRef<typeof import('./stores/settings')['hourStyle']>
|
||||
@@ -436,7 +481,9 @@ declare module 'vue' {
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||
readonly isShallow: UnwrapRef<typeof import('vue')['isShallow']>
|
||||
readonly lightTheme: UnwrapRef<typeof import('./stores/settings')['lightTheme']>
|
||||
readonly loadBetween: UnwrapRef<typeof import('./composable/eventStreams')['loadBetween']>
|
||||
readonly locale: UnwrapRef<typeof import('./stores/settings')['locale']>
|
||||
readonly loggingContextKey: UnwrapRef<typeof import('./composable/logContext')['loggingContextKey']>
|
||||
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
|
||||
@@ -456,6 +503,7 @@ declare module 'vue' {
|
||||
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
|
||||
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
|
||||
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
|
||||
readonly onElementRemoval: UnwrapRef<typeof import('@vueuse/core')['onElementRemoval']>
|
||||
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
|
||||
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
|
||||
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
|
||||
@@ -486,11 +534,11 @@ declare module 'vue' {
|
||||
readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
|
||||
readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
|
||||
readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
|
||||
readonly refManualReset: UnwrapRef<typeof import('@vueuse/core')['refManualReset']>
|
||||
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
|
||||
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
|
||||
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
|
||||
readonly scrollContextKey: UnwrapRef<typeof import('./composable/scrollContext')['scrollContextKey']>
|
||||
readonly search: UnwrapRef<typeof import('./stores/settings')['search']>
|
||||
readonly sessionHost: UnwrapRef<typeof import('./composable/storage')['sessionHost']>
|
||||
@@ -518,6 +566,7 @@ declare module 'vue' {
|
||||
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
|
||||
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||
readonly toRelativeTime: UnwrapRef<typeof import('./utils/index')['toRelativeTime']>
|
||||
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
|
||||
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
|
||||
@@ -530,6 +579,7 @@ declare module 'vue' {
|
||||
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
|
||||
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
|
||||
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
|
||||
readonly useAnnouncements: UnwrapRef<typeof import('./stores/announcements')['useAnnouncements']>
|
||||
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
|
||||
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
|
||||
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
|
||||
@@ -560,6 +610,7 @@ declare module 'vue' {
|
||||
readonly useContainerActions: UnwrapRef<typeof import('./composable/containerActions')['useContainerActions']>
|
||||
readonly useContainerStore: UnwrapRef<typeof import('./stores/container')['useContainerStore']>
|
||||
readonly useContainerStream: UnwrapRef<typeof import('./composable/eventStreams')['useContainerStream']>
|
||||
readonly useCountdown: UnwrapRef<typeof import('@vueuse/core')['useCountdown']>
|
||||
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
|
||||
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
||||
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
|
||||
@@ -577,6 +628,7 @@ declare module 'vue' {
|
||||
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
|
||||
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
|
||||
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
|
||||
readonly useDownloadUrl: UnwrapRef<typeof import('./composable/downloadUrl')['useDownloadUrl']>
|
||||
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
|
||||
readonly useDrawer: UnwrapRef<typeof import('./composable/drawer')['useDrawer']>
|
||||
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
|
||||
@@ -603,6 +655,8 @@ declare module 'vue' {
|
||||
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
|
||||
readonly useGroupedStream: UnwrapRef<typeof import('./composable/eventStreams')['useGroupedStream']>
|
||||
readonly useHead: UnwrapRef<typeof import('@vueuse/head')['useHead']>
|
||||
readonly useHistoricalContainerLog: UnwrapRef<typeof import('./composable/historicalLogs')['useHistoricalContainerLog']>
|
||||
readonly useHostStream: UnwrapRef<typeof import('./composable/eventStreams')['useHostStream']>
|
||||
readonly useHosts: UnwrapRef<typeof import('./stores/hosts')['useHosts']>
|
||||
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
|
||||
readonly useId: UnwrapRef<typeof import('vue')['useId']>
|
||||
@@ -612,6 +666,7 @@ declare module 'vue' {
|
||||
readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
|
||||
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
|
||||
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
|
||||
readonly useK8sStore: UnwrapRef<typeof import('./stores/k8s')['useK8sStore']>
|
||||
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
|
||||
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
|
||||
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
|
||||
@@ -629,12 +684,14 @@ declare module 'vue' {
|
||||
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
|
||||
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
|
||||
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
|
||||
readonly useNamespaceStream: UnwrapRef<typeof import('./composable/eventStreams')['useNamespaceStream']>
|
||||
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
|
||||
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
|
||||
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
|
||||
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
|
||||
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
|
||||
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
|
||||
readonly useOwnerStream: UnwrapRef<typeof import('./composable/eventStreams')['useOwnerStream']>
|
||||
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
|
||||
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
|
||||
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
|
||||
@@ -649,14 +706,15 @@ declare module 'vue' {
|
||||
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
|
||||
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
|
||||
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
|
||||
readonly usePreferredReducedTransparency: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedTransparency']>
|
||||
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
|
||||
readonly useProfileStorage: UnwrapRef<typeof import('./composable/profileStorage')['useProfileStorage']>
|
||||
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
|
||||
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
|
||||
readonly useReleases: UnwrapRef<typeof import('./stores/releases')['useReleases']>
|
||||
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
|
||||
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
|
||||
readonly useSSRWidth: UnwrapRef<typeof import('@vueuse/core')['useSSRWidth']>
|
||||
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
|
||||
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
|
||||
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
|
||||
@@ -690,6 +748,7 @@ declare module 'vue' {
|
||||
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
|
||||
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
|
||||
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
|
||||
readonly useTimeAgoIntl: UnwrapRef<typeof import('@vueuse/core')['useTimeAgoIntl']>
|
||||
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
|
||||
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
|
||||
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
|
||||
@@ -733,4 +792,4 @@ declare module 'vue' {
|
||||
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
|
||||
readonly withBase: UnwrapRef<typeof import('./stores/config')['withBase']>
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+45
-10
@@ -1,16 +1,22 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// biome-ignore lint: disable
|
||||
// oxlint-disable
|
||||
// ------
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
Announcements: typeof import('./components/Announcements.vue')['default']
|
||||
BarChart: typeof import('./components/BarChart.vue')['default']
|
||||
'Carbon:caretDown': typeof import('~icons/carbon/caret-down')['default']
|
||||
'Carbon:circleSolid': typeof import('~icons/carbon/circle-solid')['default']
|
||||
'Carbon:copyFile': typeof import('~icons/carbon/copy-file')['default']
|
||||
'Carbon:information': typeof import('~icons/carbon/information')['default']
|
||||
'Carbon:logoKubernetes': typeof import('~icons/carbon/logo-kubernetes')['default']
|
||||
'Carbon:macShift': typeof import('~icons/carbon/mac-shift')['default']
|
||||
'Carbon:play': typeof import('~icons/carbon/play')['default']
|
||||
'Carbon:restart': typeof import('~icons/carbon/restart')['default']
|
||||
@@ -18,67 +24,92 @@ declare module 'vue' {
|
||||
'Carbon:starFilled': typeof import('~icons/carbon/star-filled')['default']
|
||||
'Carbon:stopFilledAlt': typeof import('~icons/carbon/stop-filled-alt')['default']
|
||||
'Carbon:warning': typeof import('~icons/carbon/warning')['default']
|
||||
Carousel: typeof import('./components/common/Carousel.vue')['default']
|
||||
CarouselItem: typeof import('./components/common/CarouselItem.vue')['default']
|
||||
'Cil:checkCircle': typeof import('~icons/cil/check-circle')['default']
|
||||
'Cil:circle': typeof import('~icons/cil/circle')['default']
|
||||
'Cil:columns': typeof import('~icons/cil/columns')['default']
|
||||
'Cil:xCircle': typeof import('~icons/cil/x-circle')['default']
|
||||
ComplexLogItem: typeof import('./components/LogViewer/ComplexLogItem.vue')['default']
|
||||
ContainerActionsToolbar: typeof import('./components/ContainerViewer/ContainerActionsToolbar.vue')['default']
|
||||
ContainerDropdown: typeof import('./components/ContainerDropdown.vue')['default']
|
||||
ContainerEventLogItem: typeof import('./components/LogViewer/ContainerEventLogItem.vue')['default']
|
||||
ContainerHealth: typeof import('./components/ContainerViewer/ContainerHealth.vue')['default']
|
||||
ContainerLog: typeof import('./components/ContainerViewer/ContainerLog.vue')['default']
|
||||
ContainerName: typeof import('./components/LogViewer/ContainerName.vue')['default']
|
||||
ContainerPopup: typeof import('./components/ContainerPopup.vue')['default']
|
||||
ContainerStatCell: typeof import('./components/ContainerStatCell.vue')['default']
|
||||
ContainerTable: typeof import('./components/ContainerTable.vue')['default']
|
||||
ContainerTitle: typeof import('./components/ContainerViewer/ContainerTitle.vue')['default']
|
||||
DateTime: typeof import('./components/common/DateTime.vue')['default']
|
||||
DistanceTime: typeof import('./components/common/DistanceTime.vue')['default']
|
||||
Dropdown: typeof import('./components/common/Dropdown.vue')['default']
|
||||
DropdownMenu: typeof import('./components/common/DropdownMenu.vue')['default']
|
||||
EventSource: typeof import('./components/LogViewer/EventSource.vue')['default']
|
||||
FuzzySearchModal: typeof import('./components/FuzzySearchModal.vue')['default']
|
||||
GroupedLog: typeof import('./components/GroupedViewer/GroupedLog.vue')['default']
|
||||
GroupedLogItem: typeof import('./components/LogViewer/GroupedLogItem.vue')['default']
|
||||
GroupMenu: typeof import('./components/GroupMenu.vue')['default']
|
||||
HistoricalContainerLog: typeof import('./components/ContainerViewer/HistoricalContainerLog.vue')['default']
|
||||
HostCard: typeof import('./components/HostCard.vue')['default']
|
||||
HostIcon: typeof import('./components/common/HostIcon.vue')['default']
|
||||
HostList: typeof import('./components/HostList.vue')['default']
|
||||
HostLog: typeof import('./components/HostViewer/HostLog.vue')['default']
|
||||
HostMenu: typeof import('./components/HostMenu.vue')['default']
|
||||
'Ic:sharpKeyboardReturn': typeof import('~icons/ic/sharp-keyboard-return')['default']
|
||||
InfiniteLoader: typeof import('./components/InfiniteLoader.vue')['default']
|
||||
IndeterminateBar: typeof import('./components/common/IndeterminateBar.vue')['default']
|
||||
'Ion:ellipsisVertical': typeof import('~icons/ion/ellipsis-vertical')['default']
|
||||
K8sMenu: typeof import('./components/K8sMenu.vue')['default']
|
||||
KeyShortcut: typeof import('./components/common/KeyShortcut.vue')['default']
|
||||
LabeledInput: typeof import('./components/common/LabeledInput.vue')['default']
|
||||
Links: typeof import('./components/Links.vue')['default']
|
||||
LoadMoreLogItem: typeof import('./components/LogViewer/LoadMoreLogItem.vue')['default']
|
||||
LogActions: typeof import('./components/LogViewer/LogActions.vue')['default']
|
||||
LogAnalytics: typeof import('./components/LogViewer/LogAnalytics.vue')['default']
|
||||
LogDate: typeof import('./components/LogViewer/LogDate.vue')['default']
|
||||
LogDetails: typeof import('./components/LogViewer/LogDetails.vue')['default']
|
||||
LogItem: typeof import('./components/LogViewer/LogItem.vue')['default']
|
||||
LogLevel: typeof import('./components/LogViewer/LogLevel.vue')['default']
|
||||
LogList: typeof import('./components/LogViewer/LogList.vue')['default']
|
||||
LogMessageActions: typeof import('./components/LogViewer/LogMessageActions.vue')['default']
|
||||
LogStd: typeof import('./components/LogViewer/LogStd.vue')['default']
|
||||
LogViewer: typeof import('./components/LogViewer/LogViewer.vue')['default']
|
||||
'MaterialSymbols:codeBlocksRounded': typeof import('~icons/material-symbols/code-blocks-rounded')['default']
|
||||
'MaterialSymbols:contentCopy': typeof import('~icons/material-symbols/content-copy')['default']
|
||||
'MaterialSymbols:eyeTracking': typeof import('~icons/material-symbols/eye-tracking')['default']
|
||||
'MaterialSymbols:link': typeof import('~icons/material-symbols/link')['default']
|
||||
'MaterialSymbols:logout': typeof import('~icons/material-symbols/logout')['default']
|
||||
'MaterialSymbols:person': typeof import('~icons/material-symbols/person')['default']
|
||||
'MaterialSymbols:terminal': typeof import('~icons/material-symbols/terminal')['default']
|
||||
'MaterialSymbolsLight:collapseAll': typeof import('~icons/material-symbols-light/collapse-all')['default']
|
||||
'Mdi:account': typeof import('~icons/mdi/account')['default']
|
||||
'Mdi:announcement': typeof import('~icons/mdi/announcement')['default']
|
||||
'Mdi:arrowUp': typeof import('~icons/mdi/arrow-up')['default']
|
||||
'Mdi:beer': typeof import('~icons/mdi/beer')['default']
|
||||
'Mdi:check': typeof import('~icons/mdi/check')['default']
|
||||
'Mdi:chevronDoubleDown': typeof import('~icons/mdi/chevron-double-down')['default']
|
||||
'Mdi:chevronDown': typeof import('~icons/mdi/chevron-down')['default']
|
||||
'Mdi:chevronLeft': typeof import('~icons/mdi/chevron-left')['default']
|
||||
'Mdi:chevronRight': typeof import('~icons/mdi/chevron-right')['default']
|
||||
'Mdi:close': typeof import('~icons/mdi/close')['default']
|
||||
'Mdi:cog': typeof import('~icons/mdi/cog')['default']
|
||||
'Mdi:contentCopy': typeof import('~icons/mdi/content-copy')['default']
|
||||
'Mdi:docker': typeof import('~icons/mdi/docker')['default']
|
||||
'Mdi:gauge': typeof import('~icons/mdi/gauge')['default']
|
||||
'Mdi:github': typeof import('~icons/mdi/github')['default']
|
||||
'Mdi:hamburgerMenu': typeof import('~icons/mdi/hamburger-menu')['default']
|
||||
'Mdi:hexagonMultiple': typeof import('~icons/mdi/hexagon-multiple')['default']
|
||||
'Mdi:key': typeof import('~icons/mdi/key')['default']
|
||||
'Mdi:keyboardEsc': typeof import('~icons/mdi/keyboard-esc')['default']
|
||||
'Mdi:lightningBolt': typeof import('~icons/mdi/lightning-bolt')['default']
|
||||
'Mdi:magnify': typeof import('~icons/mdi/magnify')['default']
|
||||
'Mdi:satelliteVariant': typeof import('~icons/mdi/satellite-variant')['default']
|
||||
MetricCard: typeof import('./components/MetricCard.vue')['default']
|
||||
MobileMenu: typeof import('./components/common/MobileMenu.vue')['default']
|
||||
MultiContainerActionToolbar: typeof import('./components/LogViewer/MultiContainerActionToolbar.vue')['default']
|
||||
MultiContainerLog: typeof import('./components/MultiContainerViewer/MultiContainerLog.vue')['default']
|
||||
MultiContainerStat: typeof import('./components/LogViewer/MultiContainerStat.vue')['default']
|
||||
NamespaceLog: typeof import('./components/K8sViewer/NamespaceLog.vue')['default']
|
||||
'Octicon:container24': typeof import('~icons/octicon/container24')['default']
|
||||
'Octicon:download24': typeof import('~icons/octicon/download24')['default']
|
||||
'Octicon:trash24': typeof import('~icons/octicon/trash24')['default']
|
||||
OwnerLog: typeof import('./components/K8sViewer/OwnerLog.vue')['default']
|
||||
PageWithLinks: typeof import('./components/PageWithLinks.vue')['default']
|
||||
'Ph:arrowsMerge': typeof import('~icons/ph/arrows-merge')['default']
|
||||
'Ph:boundingBoxFill': typeof import('~icons/ph/bounding-box-fill')['default']
|
||||
@@ -86,14 +117,17 @@ declare module 'vue' {
|
||||
'Ph:command': typeof import('~icons/ph/command')['default']
|
||||
'Ph:computerTower': typeof import('~icons/ph/computer-tower')['default']
|
||||
'Ph:controlBold': typeof import('~icons/ph/control-bold')['default']
|
||||
'Ph:cpu': typeof import('~icons/ph/cpu')['default']
|
||||
'Ph:dotsThreeVerticalBold': typeof import('~icons/ph/dots-three-vertical-bold')['default']
|
||||
'Ph:fileSql': typeof import('~icons/ph/file-sql')['default']
|
||||
'Ph:globeSimple': typeof import('~icons/ph/globe-simple')['default']
|
||||
'Ph:memory': typeof import('~icons/ph/memory')['default']
|
||||
'Ph:stack': typeof import('~icons/ph/stack')['default']
|
||||
'Ph:stackSimple': typeof import('~icons/ph/stack-simple')['default']
|
||||
PhArrowDown: typeof import('~icons/ph/arrow-down')['default']
|
||||
PhArrowUp: typeof import('~icons/ph/arrow-up')['default']
|
||||
Popup: typeof import('./components/Popup.vue')['default']
|
||||
Releases: typeof import('./components/Releases.vue')['default']
|
||||
RandomColorTag: typeof import('./components/LogViewer/RandomColorTag.vue')['default']
|
||||
RelativeTime: typeof import('./components/common/RelativeTime.vue')['default']
|
||||
'Ri:terminalWindowFill': typeof import('~icons/ri/terminal-window-fill')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
ScrollableView: typeof import('./components/ScrollableView.vue')['default']
|
||||
@@ -109,10 +143,11 @@ declare module 'vue' {
|
||||
SQLTable: typeof import('./components/LogViewer/SQLTable.vue')['default']
|
||||
StackLog: typeof import('./components/StackViewer/StackLog.vue')['default']
|
||||
StatMonitor: typeof import('./components/LogViewer/StatMonitor.vue')['default']
|
||||
StatSparkline: typeof import('./components/LogViewer/StatSparkline.vue')['default']
|
||||
SwarmMenu: typeof import('./components/SwarmMenu.vue')['default']
|
||||
Tag: typeof import('./components/common/Tag.vue')['default']
|
||||
Terminal: typeof import('./components/Terminal.vue')['default']
|
||||
TimedButton: typeof import('./components/common/TimedButton.vue')['default']
|
||||
ToastModal: typeof import('./components/common/ToastModal.vue')['default']
|
||||
Toggle: typeof import('./components/common/Toggle.vue')['default']
|
||||
ViewerWithSource: typeof import('./components/LogViewer/ViewerWithSource.vue')['default']
|
||||
ZigZag: typeof import('./components/LogViewer/ZigZag.vue')['default']
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<Dropdown
|
||||
class="dropdown-end"
|
||||
@click="config.releaseCheckMode === 'manual' && fetchReleases()"
|
||||
@closed="releaseSeen = mostRecent?.tag ?? config.version"
|
||||
>
|
||||
<template #trigger>
|
||||
<mdi:announcement class="size-6 -rotate-12" />
|
||||
<template v-if="announcements.length > 0 && releaseSeen != mostRecent?.tag">
|
||||
<span class="bg-red absolute top-0 right-px size-2 animate-ping rounded-full opacity-75"></span>
|
||||
<span class="bg-red absolute top-0 right-px size-2 rounded-full"></span>
|
||||
</template>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="w-72">
|
||||
<ul class="space-y-4 p-2">
|
||||
<li v-for="release in announcements" v-if="announcements.length > 0">
|
||||
<template v-if="release.announcement">
|
||||
<div class="flex items-baseline gap-1">
|
||||
<carbon:information class="text-info self-center" />
|
||||
<a
|
||||
:href="release.htmlUrl"
|
||||
class="link-primary text-lg font-bold"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{{ release.name }}
|
||||
</a>
|
||||
<span class="ml-1 text-xs"><RelativeTime :date="release.createdAt" /></span>
|
||||
</div>
|
||||
<div class="text-base-content/80 text-sm">
|
||||
{{ release.body }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex items-baseline gap-1">
|
||||
<carbon:warning class="stroke-orange self-center" v-if="release.breaking > 0" />
|
||||
<a
|
||||
:href="release.htmlUrl"
|
||||
class="link-primary text-lg font-bold"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{{ release.name }}
|
||||
</a>
|
||||
<span class="ml-1 text-xs"><RelativeTime :date="release.createdAt" /></span>
|
||||
<Tag class="bg-red ml-auto px-1 py-1 text-xs" v-if="release.latest">
|
||||
{{ $t("releases.latest") }}
|
||||
</Tag>
|
||||
</div>
|
||||
<div class="text-base-content/80 text-sm">
|
||||
{{ summary(release) }}
|
||||
</div>
|
||||
</template>
|
||||
</li>
|
||||
<li v-else>
|
||||
<div class="text-base-content/80 text-sm">
|
||||
{{ $t("releases.no_releases") }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAnnouncements } from "@/stores/announcements";
|
||||
|
||||
const { announcements, mostRecent, fetchReleases } = useAnnouncements();
|
||||
const { t } = useI18n();
|
||||
|
||||
const releaseSeen = useProfileStorage("releaseSeen", config.version);
|
||||
|
||||
function summary(release: { features: number; bugFixes: number; breaking: number }) {
|
||||
if (release.features > 0 && release.bugFixes > 0 && release.breaking > 0) {
|
||||
return t("releases.three_parts", {
|
||||
first: t("releases.breaking", { count: release.breaking }),
|
||||
second: t("releases.features", { count: release.features }),
|
||||
third: t("releases.bugFixes", { count: release.bugFixes }),
|
||||
});
|
||||
}
|
||||
|
||||
if (release.features > 0 && release.bugFixes > 0) {
|
||||
return t("releases.two_parts", {
|
||||
first: t("releases.features", { count: release.features }),
|
||||
second: t("releases.bugFixes", { count: release.bugFixes }),
|
||||
});
|
||||
}
|
||||
|
||||
if (release.features > 0 && release.breaking > 0) {
|
||||
return t("releases.two_parts", {
|
||||
first: t("releases.features", { count: release.features }),
|
||||
second: t("releases.breaking", { count: release.breaking }),
|
||||
});
|
||||
}
|
||||
|
||||
if (release.bugFixes > 0 && release.breaking > 0) {
|
||||
return t("releases.two_parts", {
|
||||
first: t("releases.bugFixes", { count: release.bugFixes }),
|
||||
second: t("releases.breaking", { count: release.breaking }),
|
||||
});
|
||||
}
|
||||
|
||||
if (release.features > 0) {
|
||||
return t("releases.features", { count: release.features });
|
||||
}
|
||||
|
||||
if (release.bugFixes > 0) {
|
||||
return t("releases.bugFixes", { count: release.bugFixes });
|
||||
}
|
||||
|
||||
if (release.breaking > 0) {
|
||||
return t("releases.breaking", { count: release.breaking });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div ref="chartContainer" class="flex items-end gap-[2px]" @mousemove="onContainerHover">
|
||||
<div
|
||||
v-for="(dataPoint, i) in downsampledData"
|
||||
:key="i"
|
||||
class="bar min-h-px flex-1 rounded-t-sm"
|
||||
:class="barClass"
|
||||
:style="{ '--height': `${Math.min(dataPoint, 100)}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bar {
|
||||
height: var(--height);
|
||||
will-change: height;
|
||||
contain: layout;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { chartData, barClass = "" } = defineProps<{
|
||||
chartData: number[];
|
||||
barClass?: string;
|
||||
}>();
|
||||
|
||||
const hoverIndex = defineEmit<[startIndex: number, endIndex: number]>();
|
||||
|
||||
const chartContainer = ref<HTMLElement | null>(null);
|
||||
const { width } = useElementSize(chartContainer);
|
||||
|
||||
const BAR_WIDTH = 3;
|
||||
const GAP = 2;
|
||||
|
||||
const availableBars = computed(() => Math.floor(width.value / (BAR_WIDTH + GAP)));
|
||||
const bucketSize = computed(() => Math.ceil(chartData.length / availableBars.value));
|
||||
|
||||
const downsampledData = ref<number[]>([]);
|
||||
const changeCounter = ref(-1);
|
||||
|
||||
// Watch chartData changes
|
||||
watch(
|
||||
() => chartData,
|
||||
() => {
|
||||
// If changeCounter is -1, it means this is the first time the data is loaded
|
||||
if (changeCounter.value === -1) {
|
||||
recalculate();
|
||||
}
|
||||
changeCounter.value++;
|
||||
if (changeCounter.value >= bucketSize.value) {
|
||||
// Recalculate when counter reaches bucket size
|
||||
recalculate();
|
||||
changeCounter.value = 0;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Recalculate when width changes
|
||||
watch([availableBars, bucketSize], () => {
|
||||
recalculate();
|
||||
changeCounter.value = -1;
|
||||
});
|
||||
|
||||
function recalculate() {
|
||||
if (chartData.length <= availableBars.value || availableBars.value === 0) {
|
||||
downsampledData.value = [...chartData];
|
||||
return;
|
||||
}
|
||||
|
||||
const size = bucketSize.value;
|
||||
const result = [];
|
||||
|
||||
// Create complete buckets
|
||||
const numCompleteBuckets = Math.floor(chartData.length / size);
|
||||
|
||||
for (let i = 0; i < numCompleteBuckets; i++) {
|
||||
const start = i * size;
|
||||
const end = start + size;
|
||||
const bucket = chartData.slice(start, end);
|
||||
const avg = bucket.reduce((sum, val) => sum + val, 0) / bucket.length;
|
||||
result.push(avg);
|
||||
}
|
||||
|
||||
// Show only the last N bars that fit on screen
|
||||
downsampledData.value = result.slice(-availableBars.value);
|
||||
}
|
||||
|
||||
function onContainerHover(event: MouseEvent) {
|
||||
if (!chartContainer.value) return;
|
||||
|
||||
const rect = chartContainer.value.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
|
||||
// Calculate which bar the mouse is over based on position
|
||||
const barWidth = width.value / downsampledData.value.length;
|
||||
const index = Math.floor(x / barWidth);
|
||||
|
||||
// Ensure index is within bounds
|
||||
if (index < 0 || index >= downsampledData.value.length) return;
|
||||
|
||||
// Map downsampled index back to original data index range
|
||||
const numCompleteBuckets = Math.floor(chartData.length / bucketSize.value);
|
||||
const offset = Math.max(0, numCompleteBuckets - availableBars.value);
|
||||
const startIndex = (offset + index) * bucketSize.value;
|
||||
const endIndex = Math.min(startIndex + bucketSize.value - 1, chartData.length - 1);
|
||||
hoverIndex(startIndex, endIndex);
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="dropdown">
|
||||
<button tabindex="0" role="button" class="btn btn-xs md:btn-sm"><slot /> <carbon:caret-down /></button>
|
||||
<ul tabindex="0" class="dropdown-content menu rounded-box bg-base-100 border-base-content/20 border shadow-sm">
|
||||
<li v-for="other in containers">
|
||||
<router-link :to="{ name: '/container/[id]', params: { id: other.id } }" class="text-nowrap">
|
||||
<div
|
||||
class="status data-[state=exited]:status-error data-[state=running]:status-success"
|
||||
:data-state="other.state"
|
||||
></div>
|
||||
{{ other.name }}
|
||||
<div v-if="other.state === 'running'">running</div>
|
||||
<RelativeTime :date="other.finishedAt" class="text-base-content/70 text-xs" v-else />
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { type Container } from "@/models/Container";
|
||||
const { containers } = defineProps<{
|
||||
containers: Container[];
|
||||
}>();
|
||||
</script>
|
||||
@@ -1,18 +1,32 @@
|
||||
<template>
|
||||
<div>
|
||||
<span class="font-light capitalize"> RUNNING </span>
|
||||
<span class="font-semibold">
|
||||
<DistanceTime :date="container.created" strict :suffix="false" />
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-light capitalize"> Load </span>
|
||||
<span class="font-semibold"> {{ container.stat.cpu.toFixed(2) }}% </span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-light capitalize"> MEM </span>
|
||||
<span class="font-semibold"> {{ formatBytes(container.stat.memoryUsage) }} </span>
|
||||
</div>
|
||||
<table class="w-full border-separate border-spacing-x-1">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th class="text-right font-light capitalize">STATE</th>
|
||||
<td class="font-semibold uppercase">{{ container.state }}</td>
|
||||
</tr>
|
||||
<tr v-if="container.startedAt.getFullYear() > 0">
|
||||
<th class="text-right font-light capitalize">STARTED</th>
|
||||
<td class="font-semibold">
|
||||
<RelativeTime :date="container.startedAt" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="container.state != 'running' && container.finishedAt.getFullYear() > 0">
|
||||
<th class="text-right font-light capitalize">FINISHED</th>
|
||||
<td class="font-semibold">
|
||||
<RelativeTime :date="container.finishedAt" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="container.state == 'running'">
|
||||
<th class="text-right font-light capitalize">Load</th>
|
||||
<td class="font-semibold">{{ container.stat.cpu.toFixed(2) }}%</td>
|
||||
</tr>
|
||||
<tr v-if="container.state == 'running'">
|
||||
<th class="text-right font-light capitalize">MEM</th>
|
||||
<td class="font-semibold">{{ formatBytes(container.stat.memoryUsage) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<BarChart class="h-4 flex-1" :chart-data="chartData" :bar-class="barClass" />
|
||||
<span class="w-fit text-right text-sm">{{ displayValue }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Container } from "@/models/Container";
|
||||
import type { Host } from "@/stores/hosts";
|
||||
|
||||
const { container, type, host } = defineProps<{
|
||||
container: Container;
|
||||
type: "cpu" | "mem";
|
||||
host: Host;
|
||||
}>();
|
||||
|
||||
function totalCores(): number {
|
||||
if (container.cpuLimit && container.cpuLimit > 0) {
|
||||
return 1;
|
||||
}
|
||||
return host.nCPU ?? 1;
|
||||
}
|
||||
|
||||
const chartData = computed(() => {
|
||||
if (type === "cpu") {
|
||||
const cores = totalCores();
|
||||
return container.statsHistory.map((stat) => Math.min(stat.cpu / cores, 100));
|
||||
}
|
||||
return container.statsHistory.map((stat) => Math.min(stat.memory, 100));
|
||||
});
|
||||
|
||||
const averageValue = computed(() => {
|
||||
if (type === "cpu") {
|
||||
const cores = totalCores();
|
||||
return Math.min(container.movingAverage.cpu / cores, 100);
|
||||
}
|
||||
return container.movingAverage.memory;
|
||||
});
|
||||
|
||||
const displayValue = computed(() => {
|
||||
if (type === "cpu") {
|
||||
return `${averageValue.value.toFixed(0)}%`;
|
||||
}
|
||||
return formatBytes(container.movingAverage.memoryUsage);
|
||||
});
|
||||
|
||||
const barClass = computed(() => {
|
||||
const value = averageValue.value;
|
||||
if (value <= 50) return "bg-success";
|
||||
if (value <= 70) return "bg-secondary";
|
||||
if (value <= 90) return "bg-warning";
|
||||
return "bg-error";
|
||||
});
|
||||
</script>
|
||||
@@ -1,112 +1,110 @@
|
||||
!
|
||||
<template>
|
||||
<div class="flex flex-row">
|
||||
<div v-if="Object.keys(hosts).length > 1" class="flex-1">
|
||||
<div role="tablist" class="tabs-boxed tabs block" v-if="Object.keys(hosts).length < 4">
|
||||
<input
|
||||
type="radio"
|
||||
name="host"
|
||||
role="tab"
|
||||
class="tab !rounded"
|
||||
aria-label="Show All"
|
||||
v-model="selectedHost"
|
||||
:value="null"
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
name="host"
|
||||
role="tab"
|
||||
class="tab !rounded"
|
||||
:aria-label="host.name"
|
||||
v-for="host in hosts"
|
||||
:value="host.id"
|
||||
:key="host.id"
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-row">
|
||||
<div v-if="Object.keys(hosts).length > 1" class="flex-1">
|
||||
<div role="tablist" class="tabs-boxed tabs block" v-if="Object.keys(hosts).length < 4">
|
||||
<input
|
||||
type="radio"
|
||||
name="host"
|
||||
role="tab"
|
||||
class="tab rounded-sm!"
|
||||
aria-label="Show All"
|
||||
v-model="selectedHost"
|
||||
:value="null"
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
name="host"
|
||||
role="tab"
|
||||
class="tab rounded-sm!"
|
||||
:aria-label="host.name"
|
||||
v-for="host in hosts"
|
||||
:value="host.id"
|
||||
:key="host.id"
|
||||
v-model="selectedHost"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DropdownMenu
|
||||
class="btn-sm"
|
||||
v-model="selectedHost"
|
||||
:options="[
|
||||
{ label: 'Show All', value: null },
|
||||
...Object.values(hosts).map((host) => ({ label: host.name, value: host.id })),
|
||||
]"
|
||||
v-else
|
||||
/>
|
||||
</div>
|
||||
<!-- @vue-ignore -->
|
||||
<dropdown-menu
|
||||
class="btn-sm"
|
||||
v-model="selectedHost"
|
||||
:options="[
|
||||
{ label: 'Show All', value: null },
|
||||
...Object.values(hosts).map((host) => ({ label: host.name, value: host.id })),
|
||||
]"
|
||||
v-else
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1 text-right" v-show="containers.length > pageSizes[0]">
|
||||
{{ $t("label.per-page") }}
|
||||
<!-- @vue-ignore -->
|
||||
<dropdown-menu
|
||||
class="dropdown-left btn-xs md:btn-sm"
|
||||
v-model="perPage"
|
||||
:options="pageSizes.map((i) => ({ label: i.toLocaleString(), value: i }))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 text-right" v-show="containers.length > pageSizes[0]">
|
||||
{{ $t("label.per-page") }}
|
||||
|
||||
<table class="table table-lg bg-base">
|
||||
<thead>
|
||||
<tr :data-direction="direction > 0 ? 'asc' : 'desc'">
|
||||
<th
|
||||
v-for="(value, key) in fields"
|
||||
:key="key"
|
||||
@click.prevent="sort(key)"
|
||||
:class="{ 'selected-sort': key === sortField }"
|
||||
v-show="isVisible(key)"
|
||||
>
|
||||
<a class="inline-flex cursor-pointer gap-2 text-sm uppercase">
|
||||
<span>{{ $t(value.label) }}</span>
|
||||
<span class="h-4" data-icon>
|
||||
<mdi:arrow-up />
|
||||
</span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="container in paginated" :key="container.id">
|
||||
<td v-if="isVisible('name')">
|
||||
<router-link :to="{ name: '/container/[id]', params: { id: container.id } }" :title="container.name">
|
||||
{{ container.name }}
|
||||
</router-link>
|
||||
</td>
|
||||
<td v-if="isVisible('host')">{{ container.hostLabel }}</td>
|
||||
<td v-if="isVisible('state')">{{ container.state }}</td>
|
||||
<td v-if="isVisible('created')">
|
||||
<distance-time :date="container.created" strict :suffix="false"></distance-time>
|
||||
</td>
|
||||
<td v-if="isVisible('cpu')">
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<progress
|
||||
class="progress progress-primary"
|
||||
:value="Math.min(container.movingAverage.cpu, 100)"
|
||||
:max="100"
|
||||
></progress>
|
||||
<span class="text-sm">{{ container.movingAverage.cpu.toFixed(0) }}%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td v-if="isVisible('mem')">
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<progress class="progress progress-primary" :value="container.movingAverage.memory" max="100"></progress>
|
||||
<span class="text-sm">{{ container.movingAverage.memory.toFixed(0) }}%</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="p-4 text-center">
|
||||
<nav class="join" v-if="isPaginated">
|
||||
<input
|
||||
class="btn btn-square join-item"
|
||||
type="radio"
|
||||
<DropdownMenu
|
||||
class="dropdown-left btn-xs md:btn-sm"
|
||||
v-model="perPage"
|
||||
:options="pageSizes.map((i) => ({ label: i.toLocaleString(), value: i }))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-box border-base-content/10 overflow-x-auto border">
|
||||
<table class="table-md md:table-lg table-zebra table">
|
||||
<thead>
|
||||
<tr :data-direction="direction > 0 ? 'asc' : 'desc'">
|
||||
<th
|
||||
v-for="(value, key) in fields"
|
||||
:key="key"
|
||||
@click.prevent="sort(key)"
|
||||
:class="[value.customClass, { 'selected-sort': key === sortField }]"
|
||||
v-show="isVisible(key)"
|
||||
>
|
||||
<a class="inline-flex cursor-pointer gap-2 text-sm uppercase">
|
||||
<span>{{ $t(value.label) }}</span>
|
||||
<span class="h-4" data-icon>
|
||||
<mdi:arrow-up />
|
||||
</span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-base-300/30">
|
||||
<tr v-for="container in paginated" :key="container.id" v-memo="[container.id]" class="hover:bg-base-100/80!">
|
||||
<td v-if="isVisible('name')" class="max-w-80 truncate">
|
||||
<router-link :to="{ name: '/container/[id]', params: { id: container.id } }" :title="container.name">
|
||||
{{ container.name }}
|
||||
</router-link>
|
||||
</td>
|
||||
<td v-if="isVisible('host')">{{ container.hostLabel }}</td>
|
||||
<td v-if="isVisible('state')">{{ container.state }}</td>
|
||||
<td v-if="isVisible('created')">
|
||||
<RelativeTime :date="container.created" />
|
||||
</td>
|
||||
<td v-if="isVisible('cpu')">
|
||||
<ContainerStatCell :container="container" type="cpu" :host="hosts[container.host]" />
|
||||
</td>
|
||||
<td v-if="isVisible('mem')">
|
||||
<ContainerStatCell :container="container" type="mem" :host="hosts[container.host]" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="p-4 text-center">
|
||||
<nav class="join" v-if="isPaginated && totalPages <= 15">
|
||||
<input
|
||||
class="btn btn-square join-item"
|
||||
type="radio"
|
||||
v-model="currentPage"
|
||||
:aria-label="`${i}`"
|
||||
:value="i"
|
||||
v-for="i in totalPages"
|
||||
/>
|
||||
</nav>
|
||||
<DropdownMenu
|
||||
v-else-if="isPaginated"
|
||||
class="btn-sm"
|
||||
v-model="currentPage"
|
||||
:aria-label="`${i}`"
|
||||
:value="i"
|
||||
v-for="i in totalPages"
|
||||
:options="Array.from({ length: totalPages }, (_, i) => ({ label: `${i + 1}`, value: i + 1 }))"
|
||||
/>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -117,7 +115,15 @@ import { toRefs } from "@vueuse/core";
|
||||
const { hosts } = useHosts();
|
||||
const selectedHost = ref(null);
|
||||
|
||||
const fields = {
|
||||
const fields: Record<
|
||||
string,
|
||||
{
|
||||
label: string;
|
||||
sortFunc: (a: Container, b: Container) => number;
|
||||
mobileVisible: boolean;
|
||||
customClass?: string;
|
||||
}
|
||||
> = {
|
||||
name: {
|
||||
label: "label.container-name",
|
||||
sortFunc: (a: Container, b: Container) => a.name.localeCompare(b.name) * direction.value,
|
||||
@@ -127,26 +133,32 @@ const fields = {
|
||||
label: "label.host",
|
||||
sortFunc: (a: Container, b: Container) => a.hostLabel.localeCompare(b.hostLabel) * direction.value,
|
||||
mobileVisible: false,
|
||||
customClass: "w-1",
|
||||
},
|
||||
state: {
|
||||
label: "label.status",
|
||||
sortFunc: (a: Container, b: Container) => a.state.localeCompare(b.state) * direction.value,
|
||||
mobileVisible: false,
|
||||
customClass: "w-1",
|
||||
},
|
||||
created: {
|
||||
label: "label.created",
|
||||
sortFunc: (a: Container, b: Container) => (a.created.getTime() - b.created.getTime()) * direction.value,
|
||||
mobileVisible: true,
|
||||
customClass: "w-1",
|
||||
},
|
||||
cpu: {
|
||||
label: "label.avg-cpu",
|
||||
sortFunc: (a: Container, b: Container) => (a.movingAverage.cpu - b.movingAverage.cpu) * direction.value,
|
||||
mobileVisible: false,
|
||||
customClass: "min-w-48",
|
||||
},
|
||||
mem: {
|
||||
label: "label.avg-mem",
|
||||
sortFunc: (a: Container, b: Container) => (a.movingAverage.memory - b.movingAverage.memory) * direction.value,
|
||||
sortFunc: (a: Container, b: Container) =>
|
||||
(a.movingAverage.memoryUsage - b.movingAverage.memoryUsage) * direction.value,
|
||||
mobileVisible: false,
|
||||
customClass: "min-w-48",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -159,10 +171,10 @@ const perPage = useStorage("DOZZLE_TABLE_PAGE_SIZE", 15);
|
||||
const pageSizes = [15, 30, 50, 100];
|
||||
|
||||
const storage = useStorage<{ column: keys; direction: 1 | -1 }>("DOZZLE_TABLE_CONTAINERS_SORT", {
|
||||
column: "created",
|
||||
direction: -1,
|
||||
column: "created" as keys,
|
||||
direction: -1 as 1 | -1,
|
||||
});
|
||||
const { column: sortField, direction } = toRefs(storage);
|
||||
const { column: sortField, direction } = toRefs(storage.value);
|
||||
const counter = useInterval(10000);
|
||||
const filteredContainers = computed(() =>
|
||||
containers.filter((c) => selectedHost.value === null || c.host === selectedHost.value),
|
||||
@@ -196,7 +208,9 @@ function isVisible(field: keys) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
<style scoped>
|
||||
@reference "@/main.css";
|
||||
|
||||
[data-icon] {
|
||||
display: none;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
@@ -206,7 +220,7 @@ function isVisible(field: keys) {
|
||||
}
|
||||
|
||||
th {
|
||||
@apply border-b-2 border-base-lighter;
|
||||
@apply border-base-200 border-b-2;
|
||||
&.selected-sort {
|
||||
font-weight: bold;
|
||||
@apply border-primary;
|
||||
@@ -217,9 +231,6 @@ th {
|
||||
}
|
||||
|
||||
tbody td {
|
||||
max-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
<template>
|
||||
<div class="dropdown dropdown-end dropdown-hover">
|
||||
<label tabindex="0" class="btn btn-ghost btn-sm w-10 gap-0.5 px-2">
|
||||
<carbon:circle-solid class="w-2.5 text-red" v-if="streamConfig.stderr" />
|
||||
<carbon:circle-solid class="w-2.5 text-blue" v-if="streamConfig.stdout" />
|
||||
<div class="dropdown dropdown-end dropdown-hover z-20">
|
||||
<label tabindex="0" class="btn btn-ghost btn-sm w-8 gap-0 px-0 md:gap-0.5">
|
||||
<carbon:circle-solid class="text-red w-2 md:w-2.5" v-if="streamConfig.stderr" />
|
||||
<carbon:circle-solid class="text-blue w-2 md:w-2.5" v-if="streamConfig.stdout" />
|
||||
</label>
|
||||
<ul tabindex="0" class="menu dropdown-content z-50 w-52 rounded-box bg-base p-1 shadow">
|
||||
<li>
|
||||
<a @click.prevent="clear()">
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 shadow-sm"
|
||||
@click="hideMenu"
|
||||
>
|
||||
<li v-if="!historical">
|
||||
<a @click="clear()">
|
||||
<octicon:trash-24 /> {{ $t("toolbar.clear") }}
|
||||
<KeyShortcut char="k" :modifiers="['shift', 'meta']" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :href="downloadUrl" download> <octicon:download-24 /> {{ $t("toolbar.download") }} </a>
|
||||
<li v-if="enableDownload">
|
||||
<a :href="downloadUrl" download>
|
||||
<octicon:download-24 />
|
||||
{{ isFiltered ? $t("toolbar.download-filtered") : $t("toolbar.download") }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a @click.prevent="showSearch = true">
|
||||
<li v-if="!historical">
|
||||
<a @click="showSearch = true">
|
||||
<mdi:magnify /> {{ $t("toolbar.search") }}
|
||||
<KeyShortcut char="f" />
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="hasComplexLogs">
|
||||
<a @click.prevent="showDrawer(LogAnalytics, { container }, 'lg')">
|
||||
<a @click="showDrawer(LogAnalytics, { container }, 'lg')">
|
||||
<ph:file-sql /> SQL Analytics
|
||||
<KeyShortcut char="f" :modifiers="['shift', 'meta']" />
|
||||
</a>
|
||||
@@ -31,8 +38,8 @@
|
||||
<details>
|
||||
<summary>
|
||||
<div class="flex w-4">
|
||||
<carbon:circle-solid class="w-2.5 text-red" v-if="streamConfig.stderr" />
|
||||
<carbon:circle-solid class="w-2.5 text-blue" v-if="streamConfig.stdout" />
|
||||
<carbon:circle-solid class="text-red w-2.5" v-if="streamConfig.stderr" />
|
||||
<carbon:circle-solid class="text-blue w-2.5" v-if="streamConfig.stdout" />
|
||||
</div>
|
||||
Streams
|
||||
</summary>
|
||||
@@ -103,7 +110,7 @@
|
||||
</li>
|
||||
|
||||
<!-- Container Actions (Enabled via config) -->
|
||||
<template v-if="enableActions">
|
||||
<template v-if="enableActions && !historical">
|
||||
<li class="line"></li>
|
||||
<li>
|
||||
<button
|
||||
@@ -134,6 +141,24 @@
|
||||
</button>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<template v-if="enableShell && !historical">
|
||||
<li class="line"></li>
|
||||
<li>
|
||||
<a @click="showDrawer(Terminal, { container, action: 'attach' }, 'lg')">
|
||||
<ri:terminal-window-fill />
|
||||
{{ $t("toolbar.attach") }}
|
||||
<KeyShortcut char="a" :modifiers="['shift', 'meta']" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a @click="showDrawer(Terminal, { container, action: 'exec' }, 'lg')">
|
||||
<material-symbols:terminal />
|
||||
{{ $t("toolbar.shell") }}
|
||||
<KeyShortcut char="e" :modifiers="['shift', 'meta']" />
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
@@ -142,13 +167,14 @@
|
||||
import { Container } from "@/models/Container";
|
||||
import { allLevels } from "@/composable/logContext";
|
||||
import LogAnalytics from "../LogViewer/LogAnalytics.vue";
|
||||
import Terminal from "@/components/Terminal.vue";
|
||||
|
||||
const { showSearch } = useSearchFilter();
|
||||
const { enableActions } = config;
|
||||
const { enableActions, enableShell, enableDownload } = config;
|
||||
const { streamConfig, hasComplexLogs, levels } = useLoggingContext();
|
||||
const showDrawer = useDrawer();
|
||||
|
||||
const { container } = defineProps<{ container: Container }>();
|
||||
const { container, historical = false } = defineProps<{ container: Container; historical?: boolean }>();
|
||||
const clear = defineEmit();
|
||||
const { actionStates, start, stop, restart } = useContainerActions(toRef(() => container));
|
||||
|
||||
@@ -160,18 +186,24 @@ onKeyStroke("f", (e) => {
|
||||
}
|
||||
}
|
||||
});
|
||||
if (enableShell) {
|
||||
onKeyStroke("a", (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
|
||||
showDrawer(Terminal, { container, action: "attach" }, "lg");
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
const downloadParams = computed(() =>
|
||||
Object.entries(toValue(streamConfig))
|
||||
.filter(([, value]) => value)
|
||||
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), {}),
|
||||
);
|
||||
onKeyStroke("e", (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
|
||||
showDrawer(Terminal, { container, action: "exec" }, "lg");
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const downloadUrl = computed(() =>
|
||||
withBase(
|
||||
`/api/hosts/${container.host}/containers/${container.id}/logs/download?${new URLSearchParams(downloadParams.value).toString()}`,
|
||||
),
|
||||
);
|
||||
const containerRef = computed(() => [container]);
|
||||
const { downloadUrl, isFiltered } = useDownloadUrl(containerRef, streamConfig, levels);
|
||||
|
||||
const disableRestart = computed(() => actionStates.stop || actionStates.start || actionStates.restart);
|
||||
|
||||
@@ -185,11 +217,23 @@ const toggleAllLevels = computed({
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const hideMenu = (e: MouseEvent) => {
|
||||
if (e.target instanceof HTMLAnchorElement) {
|
||||
setTimeout(() => {
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
<style scoped>
|
||||
@reference "@/main.css";
|
||||
|
||||
li.line {
|
||||
@apply h-px bg-base-content/20;
|
||||
@apply bg-base-content/20 h-px;
|
||||
}
|
||||
|
||||
a {
|
||||
|
||||
@@ -14,7 +14,8 @@ defineProps<{
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
<style scoped>
|
||||
@reference "@/main.css";
|
||||
[health="unhealthy"] {
|
||||
@apply text-red;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
<template>
|
||||
<ScrollableView :scrollable="scrollable" v-if="container">
|
||||
<template #header v-if="showTitle">
|
||||
<div class="mx-2 flex items-center gap-2 md:ml-4">
|
||||
<div class="@container mx-2 flex items-center gap-1 md:ml-4 md:gap-2">
|
||||
<ContainerTitle :container="container" />
|
||||
<MultiContainerStat class="ml-auto" :containers="[container]" />
|
||||
<MultiContainerStat
|
||||
class="ml-auto lg:hidden lg:@3xl:flex"
|
||||
:containers="[container]"
|
||||
v-if="container.state === 'running'"
|
||||
/>
|
||||
|
||||
<ContainerActionsToolbar @clear="viewer?.clear()" class="mobile-hidden" :container="container" />
|
||||
<ContainerActionsToolbar @clear="viewer?.clear()" :container="container" />
|
||||
<a class="btn btn-circle btn-xs" @click="close()" v-if="closable">
|
||||
<mdi:close />
|
||||
</a>
|
||||
@@ -17,7 +21,6 @@
|
||||
:stream-source="useContainerStream"
|
||||
:entity="container"
|
||||
:visible-keys="visibleKeys"
|
||||
:show-container-name="false"
|
||||
/>
|
||||
</template>
|
||||
</ScrollableView>
|
||||
@@ -46,5 +49,8 @@ const container = store.currentContainer(toRef(() => id));
|
||||
const visibleKeys = persistentVisibleKeysForContainer(container);
|
||||
const viewer = useTemplateRef<ComponentExposed<typeof ViewerWithSource>>("viewer");
|
||||
|
||||
provideLoggingContext(toRef(() => [container.value]));
|
||||
provideLoggingContext(
|
||||
toRef(() => [container.value]),
|
||||
{ showContainerName: false, showHostname: false },
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -1,24 +1,48 @@
|
||||
<template>
|
||||
<div class="flex flex-1 gap-1.5 truncate @container md:gap-2">
|
||||
<div class="@container flex flex-1 items-center gap-1.5 md:gap-2">
|
||||
<label class="swap swap-rotate size-4">
|
||||
<input type="checkbox" v-model="pinned" />
|
||||
<carbon:star-filled class="swap-on text-secondary" />
|
||||
<carbon:star class="swap-off" />
|
||||
</label>
|
||||
<div class="inline-flex font-mono text-sm">
|
||||
<div v-if="config.hosts.length > 1" class="mobile-hidden font-thin">
|
||||
{{ container.hostLabel }}<span class="mx-2">/</span>
|
||||
</div>
|
||||
<div class="font-semibold">{{ container.name }}</div>
|
||||
<div
|
||||
class="mobile-hidden max-w-[1.5em] truncate transition-[max-width] hover:max-w-[400px]"
|
||||
v-if="container.isSwarm"
|
||||
>
|
||||
.{{ container.swarmId }}
|
||||
<div class="inline-flex items-center text-sm">
|
||||
<div class="breadcrumbs overflow-x-visible p-0 font-mono">
|
||||
<ul>
|
||||
<li v-if="config.hosts.length > 1" class="font-thin max-md:hidden">
|
||||
{{ container.hostLabel }}
|
||||
</li>
|
||||
<li>
|
||||
<template v-if="otherContainers.length === 0">{{ container.name }}</template>
|
||||
<div v-else>
|
||||
<div class="dropdown">
|
||||
<button tabindex="0" role="button" class="btn btn-xs md:btn-sm">
|
||||
{{ container.name }} <carbon:caret-down />
|
||||
</button>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="dropdown-content menu rounded-box bg-base-100 border-base-content/20 border shadow-sm"
|
||||
>
|
||||
<li v-for="other in otherContainers">
|
||||
<router-link :to="{ name: '/container/[id]', params: { id: other.id } }">
|
||||
<div
|
||||
class="status data-[state=exited]:status-error data-[state=running]:status-success"
|
||||
:data-state="other.state"
|
||||
></div>
|
||||
<div v-if="other.isSwarm">{{ other.swarmId }}</div>
|
||||
<div v-else>{{ other.name }}</div>
|
||||
<div v-if="other.state === 'running'">running</div>
|
||||
<RelativeTime :date="other.finishedAt" class="text-base-content/70 text-xs" v-else />
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<ContainerHealth :health="container.health" v-if="container.health" />
|
||||
<Tag class="mobile-hidden hidden font-mono @3xl:block" size="small">
|
||||
<Tag class="hidden! font-mono @xl:block!" size="small">
|
||||
{{ container.image.replace(/@sha.*/, "") }}
|
||||
</Tag>
|
||||
</div>
|
||||
@@ -38,4 +62,14 @@ const pinned = computed({
|
||||
}
|
||||
},
|
||||
});
|
||||
const store = useContainerStore();
|
||||
const { containers: allContainers } = storeToRefs(store);
|
||||
|
||||
const otherContainers = computed(() =>
|
||||
allContainers.value
|
||||
.filter((c) => c.name === container.name && c.id !== container.id)
|
||||
.sort((a, b) => +b.created - +a.created),
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<ScrollableView :scrollable="scrollable" v-if="container">
|
||||
<template #header v-if="showTitle">
|
||||
<div class="@container mx-2 flex items-center gap-2 md:ml-4">
|
||||
<ContainerTitle :container="container" />
|
||||
<router-link
|
||||
:to="{ name: '/container/[id]', params: { id: container.id } }"
|
||||
class="btn btn-secondary btn-sm"
|
||||
v-if="container.state === 'running'"
|
||||
>
|
||||
<mdi:lightning-bolt />
|
||||
Live Logs
|
||||
</router-link>
|
||||
|
||||
<ContainerActionsToolbar class="max-md:hidden" :container="container" historical />
|
||||
<a class="btn btn-circle btn-xs" @click="close()" v-if="closable">
|
||||
<mdi:close />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<ViewerWithSource
|
||||
ref="viewer"
|
||||
:stream-source="useHistoricalContainerLog"
|
||||
:entity="historicalContainer"
|
||||
:visible-keys="visibleKeys"
|
||||
/>
|
||||
</template>
|
||||
</ScrollableView>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
|
||||
import { HistoricalContainer } from "@/models/Container";
|
||||
import { ComponentExposed } from "vue-component-type-helpers";
|
||||
|
||||
const {
|
||||
id,
|
||||
showTitle = false,
|
||||
scrollable = false,
|
||||
closable = false,
|
||||
date,
|
||||
} = defineProps<{
|
||||
id: string;
|
||||
showTitle?: boolean;
|
||||
scrollable?: boolean;
|
||||
closable?: boolean;
|
||||
date: Date;
|
||||
}>();
|
||||
|
||||
const close = defineEmit();
|
||||
|
||||
const store = useContainerStore();
|
||||
const container = store.currentContainer(toRef(() => id));
|
||||
const historicalContainer = toRef(() => new HistoricalContainer(container.value, date));
|
||||
const visibleKeys = persistentVisibleKeysForContainer(container);
|
||||
useTemplateRef<ComponentExposed<typeof ViewerWithSource>>("viewer");
|
||||
|
||||
provideLoggingContext(
|
||||
toRef(() => [container.value]),
|
||||
{ showContainerName: false, showHostname: false, historical: true },
|
||||
);
|
||||
</script>
|
||||
@@ -31,9 +31,51 @@ function createFuzzySearchModal() {
|
||||
initialState: {
|
||||
container: {
|
||||
containers: [
|
||||
new Container("123", new Date(), "image", "test", "command", "host", {}, "running", []),
|
||||
new Container("345", new Date(), "image", "foo bar", "command", "host", {}, "running", []),
|
||||
new Container("567", new Date(), "image", "baz", "command", "host", {}, "running", []),
|
||||
new Container(
|
||||
"123",
|
||||
new Date(),
|
||||
new Date(),
|
||||
new Date(),
|
||||
"image",
|
||||
"test",
|
||||
"command",
|
||||
"host",
|
||||
{},
|
||||
"running",
|
||||
0,
|
||||
0,
|
||||
[],
|
||||
),
|
||||
new Container(
|
||||
"345",
|
||||
new Date(),
|
||||
new Date(),
|
||||
new Date(),
|
||||
"image",
|
||||
"foo bar",
|
||||
"command",
|
||||
"host",
|
||||
{},
|
||||
"running",
|
||||
0,
|
||||
0,
|
||||
[],
|
||||
),
|
||||
new Container(
|
||||
"567",
|
||||
new Date(),
|
||||
new Date(),
|
||||
new Date(),
|
||||
"image",
|
||||
"baz",
|
||||
"command",
|
||||
"host",
|
||||
{},
|
||||
"running",
|
||||
0,
|
||||
0,
|
||||
[],
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="dropdown dropdown-open w-full">
|
||||
<div class="input input-primary flex h-auto items-center">
|
||||
<div class="dropdown dropdown-open w-full shadow-md">
|
||||
<div class="input input-xl input-primary flex w-full items-center">
|
||||
<mdi:magnify class="flex size-8" />
|
||||
<input
|
||||
tabindex="0"
|
||||
class="input input-lg input-ghost flex-1 px-1"
|
||||
class="input-ghost flex-1 px-1"
|
||||
ref="input"
|
||||
@keydown.down="selectedIndex = Math.min(selectedIndex + 1, data.length - 1)"
|
||||
@keydown.up="selectedIndex = Math.max(selectedIndex - 1, 0)"
|
||||
@@ -13,23 +13,27 @@
|
||||
v-model="query"
|
||||
:placeholder="$t('placeholder.search-containers')"
|
||||
/>
|
||||
<form method="dialog">
|
||||
<button class="swap swap-rotate hover:swap-active">
|
||||
<form method="dialog" class="flex">
|
||||
<button v-if="isMobile">
|
||||
<mdi:close />
|
||||
</button>
|
||||
<button v-else class="swap hover:swap-active outline-hidden">
|
||||
<mdi:keyboard-esc class="swap-off" />
|
||||
<mdi:close class="swap-on" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div
|
||||
class="dropdown-content !relative mt-2 max-h-[calc(100dvh-20rem)] w-full overflow-y-scroll rounded-md border-y-8 border-transparent bg-base-lighter px-2"
|
||||
class="dropdown-content bg-base-100 relative! mt-2 max-h-[calc(100dvh-20rem)] w-full overflow-y-scroll rounded-md border-y-8 border-transparent px-2"
|
||||
tabindex="0"
|
||||
v-if="results.length"
|
||||
>
|
||||
<ul tabindex="0" class="menu">
|
||||
<ul class="menu w-auto">
|
||||
<li v-for="(result, index) in data" ref="listItems">
|
||||
<a
|
||||
class="grid auto-cols-max grid-cols-[min-content,auto] gap-2 py-4"
|
||||
class="grid auto-cols-max grid-cols-[min-content_auto] gap-2 py-4"
|
||||
@click.prevent="selected(result.item)"
|
||||
:class="index === selectedIndex ? 'focus' : ''"
|
||||
:class="{ 'menu-focus': index === selectedIndex }"
|
||||
>
|
||||
<div :class="{ 'text-primary': result.item.state === 'running' }">
|
||||
<template v-if="result.item.type === 'container'">
|
||||
@@ -49,7 +53,7 @@
|
||||
<span data-name v-html="matchedName(result)"></span>
|
||||
</div>
|
||||
|
||||
<DistanceTime :date="result.item.created" class="text-xs font-light" />
|
||||
<RelativeTime :date="result.item.created" class="text-xs font-light" />
|
||||
<span
|
||||
@click.stop.prevent="addColumn(result.item)"
|
||||
:title="$t('tooltip.pin-column')"
|
||||
@@ -85,6 +89,15 @@ const { visibleContainers } = storeToRefs(containerStore);
|
||||
const swarmStore = useSwarmStore();
|
||||
const { stacks, services } = storeToRefs(swarmStore);
|
||||
|
||||
onMounted(async () => {
|
||||
const dialog = input.value?.closest("dialog");
|
||||
if (dialog) {
|
||||
const animations = dialog.getAnimations();
|
||||
await Promise.all(animations.map((animation) => animation.finished));
|
||||
input.value?.focus();
|
||||
}
|
||||
});
|
||||
|
||||
type Item = {
|
||||
id: string;
|
||||
created: Date;
|
||||
@@ -167,8 +180,6 @@ watch(selectedIndex, () => {
|
||||
listItems.value?.[selectedIndex.value].scrollIntoView({ block: "end" });
|
||||
});
|
||||
|
||||
useFocus(input, { initialValue: true });
|
||||
|
||||
function selected(item: Item) {
|
||||
if (item.type === "container") {
|
||||
router.push({ name: "/container/[id]", params: { id: item.id } });
|
||||
@@ -179,6 +190,7 @@ function selected(item: Item) {
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
function addColumn(container: { id: string }) {
|
||||
pinnedStore.pinContainer(container);
|
||||
close();
|
||||
@@ -204,7 +216,8 @@ function matchedName({ item, matches = [] }: FuseResult<Item>) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
<style scoped>
|
||||
@reference "@/main.css";
|
||||
:deep(mark) {
|
||||
@apply bg-transparent text-inherit underline underline-offset-2;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<ul class="menu w-full p-0 text-[0.95rem]">
|
||||
<li v-if="customGroups.length > 0">
|
||||
<details open>
|
||||
<summary class="text-base-content/80 font-light">
|
||||
<ph:bounding-box-fill />
|
||||
{{ $t("label.group-menu") }}
|
||||
</summary>
|
||||
<ul>
|
||||
<li v-for="group in customGroups" :key="group.name">
|
||||
<router-link :to="{ name: '/group/[name]', params: { name: group.name } }" active-class="menu-active">
|
||||
<ph:stack-simple />
|
||||
<div class="truncate">
|
||||
{{ group.name }}
|
||||
</div>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const store = useSwarmStore();
|
||||
|
||||
const { customGroups } = storeToRefs(store);
|
||||
</script>
|
||||
<style scoped></style>
|
||||
@@ -2,13 +2,11 @@
|
||||
<ScrollableView :scrollable="scrollable" v-if="group.containers.length && ready">
|
||||
<template #header>
|
||||
<div class="mx-2 flex items-center gap-2 md:ml-4">
|
||||
<div class="flex flex-1 gap-1.5 truncate @container md:gap-2">
|
||||
<div class="inline-flex font-mono text-sm">
|
||||
<div class="font-semibold">{{ group.containers.length }} containers</div>
|
||||
</div>
|
||||
</div>
|
||||
<ContainerDropdown :containers="group.containers">
|
||||
{{ $t("label.container", group.containers.length) }}
|
||||
</ContainerDropdown>
|
||||
<MultiContainerStat class="ml-auto" :containers="group.containers" />
|
||||
<MultiContainerActionToolbar class="mobile-hidden" @clear="viewer?.clear()" />
|
||||
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
@@ -17,7 +15,6 @@
|
||||
:stream-source="useGroupedStream"
|
||||
:entity="group"
|
||||
:visible-keys="new Map<string[], boolean>()"
|
||||
:show-container-name="true"
|
||||
/>
|
||||
</template>
|
||||
</ScrollableView>
|
||||
@@ -43,5 +40,8 @@ const { customGroups } = storeToRefs(swarmStore);
|
||||
|
||||
const group = computed(() => customGroups.value.find((g) => g.name === name) ?? new GroupedContainers("", []));
|
||||
|
||||
provideLoggingContext(toRef(() => group.value.containers));
|
||||
provideLoggingContext(
|
||||
toRef(() => group.value.containers),
|
||||
{ showContainerName: true, showHostname: false },
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<div class="card bg-base-100">
|
||||
<div class="card-body flex gap-2 max-md:p-4">
|
||||
<div class="flex flex-row gap-2 overflow-hidden">
|
||||
<div class="flex items-center gap-1 truncate text-xl font-semibold">
|
||||
<HostIcon :type="host.type" class="flex-none" />
|
||||
<div class="truncate">
|
||||
{{ host.name }}
|
||||
</div>
|
||||
|
||||
<span class="badge badge-error badge-xs gap-2 p-2" v-if="!host.available">
|
||||
<carbon:warning />
|
||||
offline
|
||||
</span>
|
||||
<span
|
||||
class="badge badge-success badge-xs gap-2 p-2"
|
||||
:class="{ 'badge-warning': config.version != host.agentVersion }"
|
||||
v-else-if="host.type == 'agent'"
|
||||
title="Dozzle Agent"
|
||||
>
|
||||
{{ host.agentVersion }}
|
||||
</span>
|
||||
</div>
|
||||
<ul class="ml-auto flex flex-row flex-wrap gap-x-2 text-sm max-md:text-xs md:gap-3">
|
||||
<li class="flex items-center gap-1">
|
||||
<octicon:container-24 class="inline-block" />
|
||||
{{ $t("label.container", hostContainers.length) }}
|
||||
</li>
|
||||
<li class="flex items-center gap-1"><mdi:docker class="inline-block" /> {{ host.dockerVersion }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2 md:gap-3" v-if="stats">
|
||||
<MetricCard
|
||||
:icon="PhCpu"
|
||||
:value="stats.weighted.movingAverage.totalCPU"
|
||||
:chartData="cpuHistory"
|
||||
container-class="border-primary/40 bg-primary/20"
|
||||
text-class="text-primary"
|
||||
bar-class="bg-primary"
|
||||
:formatValue="(value) => `${value.toFixed(1)}%`"
|
||||
:label="`${host.nCPU} CPU`"
|
||||
/>
|
||||
|
||||
<MetricCard
|
||||
:icon="PhMemory"
|
||||
:value="stats.weighted.movingAverage.totalMemUsage"
|
||||
:chartData="memHistory"
|
||||
container-class="border-secondary/40 bg-secondary/20"
|
||||
text-class="text-secondary"
|
||||
bar-class="bg-secondary"
|
||||
:formatValue="(value) => formatBytes(value, { decimals: 1 })"
|
||||
:label="formatBytes(host.memTotal, { decimals: 1 })"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Host } from "@/stores/hosts";
|
||||
import { Container } from "@/models/Container";
|
||||
// @ts-ignore
|
||||
import PhCpu from "~icons/ph/cpu";
|
||||
// @ts-ignore
|
||||
import PhMemory from "~icons/ph/memory";
|
||||
|
||||
const props = defineProps<{
|
||||
host: Host;
|
||||
}>();
|
||||
|
||||
const containerStore = useContainerStore();
|
||||
const { containers } = storeToRefs(containerStore) as unknown as {
|
||||
containers: Ref<Container[]>;
|
||||
};
|
||||
|
||||
const hostContainers = computed(() =>
|
||||
containers.value.filter((container) => container.host === props.host.id && container.state === "running"),
|
||||
);
|
||||
|
||||
function toContainerCores(container: Container): number {
|
||||
if (container.cpuLimit && container.cpuLimit > 0) {
|
||||
return 1;
|
||||
}
|
||||
return props.host.nCPU ?? 1;
|
||||
}
|
||||
|
||||
type TotalStat = {
|
||||
totalCPU: number;
|
||||
totalMem: number;
|
||||
totalMemUsage: number;
|
||||
};
|
||||
|
||||
const totalStat = ref<TotalStat>({ totalCPU: 0, totalMem: 0, totalMemUsage: 0 });
|
||||
const { history, reset } = useSimpleRefHistory(totalStat, { capacity: 300 });
|
||||
|
||||
const cpuHistory = computed(() =>
|
||||
history.value.map((stat) => ({
|
||||
percent: stat.totalCPU,
|
||||
value: stat.totalCPU,
|
||||
})),
|
||||
);
|
||||
const memHistory = computed(() =>
|
||||
history.value.map((stat) => ({
|
||||
percent: stat.totalMem,
|
||||
value: stat.totalMemUsage,
|
||||
})),
|
||||
);
|
||||
|
||||
const stats = reactive({ mostRecent: totalStat, weighted: useExponentialMovingAverage(totalStat) });
|
||||
|
||||
watch(
|
||||
() => hostContainers.value,
|
||||
() => {
|
||||
const initial: TotalStat[] = [];
|
||||
|
||||
for (let i = 1; i <= 300; i++) {
|
||||
const stat = hostContainers.value.reduce(
|
||||
(acc, container) => {
|
||||
const item = container.statsHistory.at(-i);
|
||||
if (!item) {
|
||||
return acc;
|
||||
}
|
||||
const cores = toContainerCores(container);
|
||||
return {
|
||||
totalCPU: acc.totalCPU + item.cpu / cores,
|
||||
totalMem: acc.totalMem + item.memory,
|
||||
totalMemUsage: acc.totalMemUsage + item.memoryUsage,
|
||||
};
|
||||
},
|
||||
{ totalCPU: 0, totalMem: 0, totalMemUsage: 0 },
|
||||
);
|
||||
initial.push(stat);
|
||||
}
|
||||
reset({ initial: initial.reverse() });
|
||||
stats.weighted.reset(initial.at(-1)!);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
useIntervalFn(() => {
|
||||
totalStat.value = hostContainers.value.reduce(
|
||||
(acc, container) => {
|
||||
const cores = toContainerCores(container);
|
||||
return {
|
||||
totalCPU: acc.totalCPU + container.stat.cpu / cores,
|
||||
totalMem: acc.totalMem + container.stat.memory,
|
||||
totalMemUsage: acc.totalMemUsage + container.stat.memoryUsage,
|
||||
};
|
||||
},
|
||||
{ totalCPU: 0, totalMem: 0, totalMemUsage: 0 },
|
||||
);
|
||||
}, 1000);
|
||||
</script>
|
||||
@@ -1,118 +1,11 @@
|
||||
<template>
|
||||
<ul class="grid gap-4 md:grid-cols-[repeat(auto-fill,minmax(480px,1fr))]">
|
||||
<li v-for="host in hosts" class="card bg-base-lighter">
|
||||
<div class="card-body grid auto-cols-auto grid-flow-col justify-between gap-4">
|
||||
<div class="flex flex-col gap-2 overflow-hidden">
|
||||
<div class="flex items-center gap-1 truncate text-xl font-semibold">
|
||||
<HostIcon :type="host.type" class="flex-none" />
|
||||
<div class="truncate">
|
||||
{{ host.name }}
|
||||
</div>
|
||||
|
||||
<span class="badge badge-error badge-xs gap-2 p-2" v-if="!host.available">
|
||||
<carbon:warning />
|
||||
offline
|
||||
</span>
|
||||
<span
|
||||
class="badge badge-success badge-xs gap-2 p-2"
|
||||
:class="{ 'badge-warning': config.version != host.agentVersion }"
|
||||
v-else-if="host.type == 'agent'"
|
||||
title="Dozzle Agent"
|
||||
>
|
||||
{{ host.agentVersion }}
|
||||
</span>
|
||||
</div>
|
||||
<ul class="flex flex-row gap-x-2 text-sm md:gap-3">
|
||||
<li class="flex items-center gap-1"><ph:cpu /> {{ host.nCPU }} <span class="mobile-hidden">CPUs</span></li>
|
||||
<li class="flex items-center gap-1">
|
||||
<ph:memory /> {{ formatBytes(host.memTotal) }}
|
||||
<span class="mobile-hidden">total</span>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="flex flex-row flex-wrap gap-x-2 text-sm md:gap-3">
|
||||
<li class="flex items-center gap-1">
|
||||
<octicon:container-24 class="inline-block" /> {{ $t("label.container", hostContainers[host.id]?.length) }}
|
||||
</li>
|
||||
<li class="flex items-center gap-1"><mdi:docker class="inline-block" /> {{ host.dockerVersion }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-4 md:gap-8" v-if="weightedStats[host.id]">
|
||||
<div
|
||||
class="radial-progress text-sm text-primary [--size:4rem] [--thickness:0.25em] md:text-[1rem] md:[--size:5rem]"
|
||||
:style="`--value: ${Math.floor((weightedStats[host.id].weighted.totalCPU / (host.nCPU * 100)) * 100)}; `"
|
||||
role="progressbar"
|
||||
>
|
||||
{{ weightedStats[host.id].weighted.totalCPU.toFixed(0) }}%
|
||||
</div>
|
||||
<div
|
||||
class="radial-progress text-sm text-primary [--size:4rem] [--thickness:0.25em] md:text-[1rem] md:[--size:5rem]"
|
||||
:style="`--value: ${(weightedStats[host.id].weighted.totalMem / host.memTotal) * 100};`"
|
||||
role="progressbar"
|
||||
>
|
||||
{{ formatBytes(weightedStats[host.id].weighted.totalMem, 1) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<li v-for="host in hosts" :key="host.id">
|
||||
<HostCard :host="host" />
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Container } from "@/models/Container";
|
||||
|
||||
const containerStore = useContainerStore();
|
||||
const { containers } = storeToRefs(containerStore) as unknown as {
|
||||
containers: Ref<Container[]>;
|
||||
};
|
||||
|
||||
const runningContainers = computed(() => containers.value.filter((container) => container.state === "running"));
|
||||
|
||||
const { hosts } = useHosts();
|
||||
const hostContainers = computed(() => {
|
||||
const results: Record<string, Container[]> = {};
|
||||
for (const container of runningContainers.value) {
|
||||
if (!results[container.host]) {
|
||||
results[container.host] = [];
|
||||
}
|
||||
results[container.host].push(container);
|
||||
}
|
||||
return results;
|
||||
});
|
||||
|
||||
type TotalStat = {
|
||||
totalCPU: number;
|
||||
totalMem: number;
|
||||
};
|
||||
const weightedStats: Record<string, { mostRecent: TotalStat; weighted: TotalStat }> = {};
|
||||
const initWeightedStats = () => {
|
||||
for (const [host, containers] of Object.entries(hostContainers.value)) {
|
||||
const mostRecent = ref<TotalStat>({ totalCPU: 0, totalMem: 0 });
|
||||
for (const container of containers) {
|
||||
mostRecent.value.totalCPU += container.stat.cpu;
|
||||
mostRecent.value.totalMem += container.stat.memoryUsage;
|
||||
}
|
||||
weightedStats[host] = reactive({ mostRecent, weighted: useExponentialMovingAverage(mostRecent) });
|
||||
}
|
||||
};
|
||||
|
||||
watchOnce(hostContainers, initWeightedStats);
|
||||
initWeightedStats();
|
||||
|
||||
useIntervalFn(
|
||||
() => {
|
||||
for (const [host, containers] of Object.entries(hostContainers.value)) {
|
||||
const stat = { totalCPU: 0, totalMem: 0 };
|
||||
for (const container of containers) {
|
||||
stat.totalCPU += container.stat.cpu;
|
||||
stat.totalMem += container.stat.memoryUsage;
|
||||
}
|
||||
weightedStats[host].mostRecent = stat;
|
||||
}
|
||||
},
|
||||
1000,
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,20 +1,56 @@
|
||||
<template>
|
||||
<div class="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<a @click.prevent="setHost(null)" class="link-primary">{{ $t("label.hosts") }}</a>
|
||||
</li>
|
||||
<li v-if="sessionHost && hosts[sessionHost]" class="cursor-default">
|
||||
{{ hosts[sessionHost].name }}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="flex items-center">
|
||||
<div class="breadcrumbs flex-1">
|
||||
<ul>
|
||||
<li>
|
||||
<a @click.prevent="setHost(null)" class="link-primary">{{ $t("label.hosts") }}</a>
|
||||
</li>
|
||||
<li v-if="sessionHost && hosts[sessionHost]" class="cursor-default">
|
||||
<router-link
|
||||
:to="{
|
||||
name: '/host/[id]',
|
||||
params: { id: hosts[sessionHost].id },
|
||||
}"
|
||||
class="btn btn-outline btn-primary btn-xs"
|
||||
active-class="btn-active"
|
||||
:title="$t('tooltip.merge-all')"
|
||||
>
|
||||
<ph:arrows-merge />
|
||||
{{ hosts[sessionHost].name }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<div class="dropdown dropdown-end dropdown-hover">
|
||||
<label tabindex="0" class="btn btn-square btn-ghost btn-sm">
|
||||
<ion:ellipsis-vertical />
|
||||
</label>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 shadow-sm"
|
||||
>
|
||||
<li>
|
||||
<a class="text-sm capitalize" @click="toggleShowAllContainers()">
|
||||
<mdi:check class="w-4" v-if="showAllContainers" />
|
||||
<div v-else class="w-4"></div>
|
||||
{{ $t("label.show-all-containers") }}
|
||||
</a>
|
||||
<a class="text-sm capitalize" @click="collapseAll()">
|
||||
<material-symbols-light:collapse-all class="w-4" />
|
||||
{{ $t("label.collapse-all") }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SlideTransition :slide-right="!!sessionHost">
|
||||
<template #left>
|
||||
<ul class="menu p-0">
|
||||
<li v-for="host in hosts" :key="host.id">
|
||||
<a @click.prevent="setHost(host.id)" :class="{ 'pointer-events-none text-base-content/50': !host.available }">
|
||||
<a @click.prevent="setHost(host.id)" :class="{ 'text-base-content/50 pointer-events-none': !host.available }">
|
||||
<HostIcon :type="host.type" />
|
||||
{{ host.name }}
|
||||
<span class="badge badge-error badge-xs p-1.5" v-if="!host.available">offline</span>
|
||||
@@ -23,12 +59,12 @@
|
||||
</ul>
|
||||
</template>
|
||||
<template #right>
|
||||
<ul class="containers menu p-0 [&_li.menu-title]:px-0">
|
||||
<ul class="containers menu w-full p-0 [&_li.menu-title]:px-0">
|
||||
<li v-for="{ label, containers, icon } in menuItems" :key="label">
|
||||
<details :open="!collapsedGroups.has(label)" @toggle="updateCollapsedGroups($event, label)">
|
||||
<summary class="font-light text-base-content/80">
|
||||
<summary class="text-base-content/80 font-light">
|
||||
<component :is="icon" />
|
||||
{{ label.startsWith("label.") ? $t(label) : label }}
|
||||
{{ label.startsWith("label.") ? $t(label) : label }} ({{ containers.length }})
|
||||
|
||||
<router-link
|
||||
:to="{
|
||||
@@ -37,7 +73,7 @@
|
||||
}"
|
||||
class="btn btn-square btn-outline btn-primary btn-xs"
|
||||
active-class="btn-active"
|
||||
:title="$t('tooltip.merge-containers')"
|
||||
:title="$t('tooltip.merge-all')"
|
||||
>
|
||||
<ph:arrows-merge />
|
||||
</router-link>
|
||||
@@ -47,17 +83,21 @@
|
||||
<Popup>
|
||||
<router-link
|
||||
:to="{ name: '/container/[id]', params: { id: item.id } }"
|
||||
active-class="active-primary"
|
||||
active-class="menu-active"
|
||||
@click.alt.stop.prevent="pinnedStore.pinContainer(item)"
|
||||
:title="item.name"
|
||||
class="group auto-cols-[auto_max-content_max-content]"
|
||||
class="group auto-cols-[content_max_auto_max-content_max-content]"
|
||||
>
|
||||
<div
|
||||
class="status data-[state=exited]:status-error data-[state=running]:status-success"
|
||||
:data-state="item.state"
|
||||
></div>
|
||||
<div class="truncate">
|
||||
{{ item.name }}<span class="font-light opacity-70" v-if="item.isSwarm">.{{ item.swarmId }}</span>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<ContainerHealth :health="item.health" />
|
||||
<span
|
||||
class="hidden hover:text-secondary group-hover:inline-block"
|
||||
class="hover:text-secondary hidden group-hover:inline-block"
|
||||
@click.stop.prevent="pinnedStore.pinContainer(item)"
|
||||
v-show="!pinnedStore.isPinned(item)"
|
||||
:title="$t('tooltip.pin-column')"
|
||||
@@ -81,6 +121,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { Container } from "@/models/Container";
|
||||
import { sessionHost } from "@/composable/storage";
|
||||
import { showAllContainers, groupContainers } from "@/stores/settings";
|
||||
|
||||
// @ts-ignore
|
||||
import Pin from "~icons/ph/map-pin-simple";
|
||||
@@ -106,6 +147,18 @@ const updateCollapsedGroups = (event: Event, label: string) => {
|
||||
} else {
|
||||
collapsedGroups.value.add(label);
|
||||
}
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
};
|
||||
|
||||
const collapseAll = () => {
|
||||
menuItems.value.forEach(({ label }) => {
|
||||
collapsedGroups.value.add(label);
|
||||
});
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedPinnedContainers = debouncedRef(pinnedContainers, 200);
|
||||
@@ -130,7 +183,7 @@ const menuItems = computed(() => {
|
||||
|
||||
for (const item of sortedContainers.value) {
|
||||
const namespace = item.namespace;
|
||||
if (debouncedPinnedContainers.value.has(item.name)) {
|
||||
if (debouncedPinnedContainers.value?.has(item.name)) {
|
||||
pinned.push(item);
|
||||
} else if (namespace) {
|
||||
namespaced[namespace] ||= [];
|
||||
@@ -145,10 +198,15 @@ const menuItems = computed(() => {
|
||||
items.push({ label: "label.pinned", containers: pinned, icon: Pin });
|
||||
}
|
||||
for (const [label, containers] of Object.entries(namespaced).sort(([a], [b]) => a.localeCompare(b))) {
|
||||
if (containers.length > 1) {
|
||||
const shouldGroup =
|
||||
groupContainers.value === "always" || (groupContainers.value === "at-least-2" && containers.length > 1);
|
||||
|
||||
if (shouldGroup) {
|
||||
items.push({ label, containers, icon: Stack });
|
||||
} else {
|
||||
singular.push(containers[0]);
|
||||
for (const container of containers) {
|
||||
singular.push(container);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,14 +233,16 @@ watchEffect(() => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const toggleShowAllContainers = () => (showAllContainers.value = !showAllContainers.value);
|
||||
</script>
|
||||
<style scoped lang="postcss">
|
||||
<style scoped>
|
||||
.menu {
|
||||
@apply text-[0.95rem];
|
||||
}
|
||||
|
||||
li.exited {
|
||||
@apply opacity-50;
|
||||
@apply opacity-75;
|
||||
}
|
||||
|
||||
li.deleted {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<ScrollableView :scrollable="scrollable" v-if="host">
|
||||
<template #header>
|
||||
<div class="mx-2 flex items-center gap-2 md:ml-4">
|
||||
<div class="flex flex-1 items-center gap-1.5 truncate md:gap-2">
|
||||
<ph:computer-tower />
|
||||
<div class="inline-flex font-mono text-sm">
|
||||
<div class="font-semibold">{{ host.name }}</div>
|
||||
</div>
|
||||
<Tag class="font-mono max-md:hidden" size="small">
|
||||
{{ $t("label.container", containers.length) }}
|
||||
</Tag>
|
||||
</div>
|
||||
<MultiContainerStat class="ml-auto" :containers="containers" />
|
||||
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<ViewerWithSource
|
||||
ref="viewer"
|
||||
:stream-source="useHostStream"
|
||||
:entity="host"
|
||||
:visible-keys="new Map<string[], boolean>()"
|
||||
/>
|
||||
</template>
|
||||
</ScrollableView>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
|
||||
import { ComponentExposed } from "vue-component-type-helpers";
|
||||
const { id, scrollable = false } = defineProps<{
|
||||
id: string;
|
||||
scrollable?: boolean;
|
||||
}>();
|
||||
const store = useContainerStore();
|
||||
const { containersByHost } = storeToRefs(store);
|
||||
const { hosts } = useHosts();
|
||||
const host = computed(() => hosts.value[id]);
|
||||
const containers = computed(() => containersByHost.value?.[id]?.filter((c) => c.state === "running") ?? []);
|
||||
const viewer = useTemplateRef<ComponentExposed<typeof ViewerWithSource>>("viewer");
|
||||
provideLoggingContext(containers, { showContainerName: true, showHostname: false });
|
||||
</script>
|
||||
@@ -1,31 +0,0 @@
|
||||
<template>
|
||||
<div ref="root" class="flex min-h-[1px] justify-center">
|
||||
<span class="loading loading-bars loading-md mt-4 text-primary" v-show="isLoading"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const { onLoadMore = () => Promise.resolve(), enabled } = defineProps<{
|
||||
onLoadMore: () => Promise<void>;
|
||||
enabled: boolean;
|
||||
}>();
|
||||
|
||||
const isLoading = ref(false);
|
||||
const root = ref<HTMLElement>();
|
||||
|
||||
const observer = new IntersectionObserver(async (entries) => {
|
||||
if (entries[0].intersectionRatio <= 0) return;
|
||||
if (onLoadMore && enabled) {
|
||||
const scrollingParent = root.value?.closest("[data-scrolling]") || document.documentElement;
|
||||
const previousHeight = scrollingParent.scrollHeight;
|
||||
isLoading.value = true;
|
||||
await onLoadMore();
|
||||
isLoading.value = false;
|
||||
await nextTick();
|
||||
scrollingParent.scrollTop += scrollingParent.scrollHeight - previousHeight;
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => observer.observe(root.value!));
|
||||
onUnmounted(() => observer.disconnect());
|
||||
</script>
|
||||
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<div class="breadcrumbs flex-1">
|
||||
<ul>
|
||||
<li>
|
||||
<a @click.prevent="setNamespace(null)" class="link-primary">{{ $t("label.namespaces") }}</a>
|
||||
</li>
|
||||
<li v-if="selectedNamespace === 'all'">
|
||||
{{ $t("label.all-namespaces") }}
|
||||
</li>
|
||||
<li v-else-if="selectedNamespace" class="cursor-default">
|
||||
<router-link
|
||||
:to="{
|
||||
name: '/namespace/[name]',
|
||||
params: { name: selectedNamespace },
|
||||
}"
|
||||
class="btn btn-outline btn-primary btn-xs"
|
||||
active-class="btn-active"
|
||||
>
|
||||
<ph:arrows-merge />
|
||||
{{ selectedNamespace }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<div class="dropdown dropdown-end dropdown-hover">
|
||||
<label tabindex="0" class="btn btn-square btn-ghost btn-sm">
|
||||
<ph:dots-three-vertical-bold />
|
||||
</label>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 shadow-sm"
|
||||
>
|
||||
<li>
|
||||
<a class="text-sm capitalize" @click="collapseAll()">
|
||||
<material-symbols-light:collapse-all class="w-4" />
|
||||
{{ $t("label.collapse-all") }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SlideTransition :slide-right="selectedNamespace !== null">
|
||||
<template #left>
|
||||
<ul class="menu p-0">
|
||||
<li>
|
||||
<a @click.prevent="setNamespace('all')">
|
||||
<ph:circles-four />
|
||||
{{ $t("label.all-namespaces") }}
|
||||
</a>
|
||||
</li>
|
||||
<li v-for="ns in namespaces" :key="ns.name">
|
||||
<a @click.prevent="setNamespace(ns.name)">
|
||||
<ph:circles-four />
|
||||
{{ ns.name }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<template #right>
|
||||
<ul class="menu w-full p-0 text-[0.95rem]" ref="menu">
|
||||
<li v-for="{ name, owners } in filteredNamespaces" :key="name">
|
||||
<details open>
|
||||
<summary class="text-base-content/80 font-light">
|
||||
<ph:stack />
|
||||
{{ name }} ({{ owners.length }})
|
||||
|
||||
<router-link
|
||||
:to="{ name: '/namespace/[name]', params: { name } }"
|
||||
class="btn btn-square btn-outline btn-primary btn-xs"
|
||||
active-class="btn-active"
|
||||
:title="$t('tooltip.merge-all')"
|
||||
>
|
||||
<ph:arrows-merge />
|
||||
</router-link>
|
||||
</summary>
|
||||
<ul>
|
||||
<li v-for="owner in owners" :key="`${owner.kind}-${owner.name}`">
|
||||
<router-link :to="{ name: '/owner/[name]', params: { name: owner.name } }" active-class="menu-active">
|
||||
<ph:stack-simple />
|
||||
<div class="truncate">{{ owner.kind }}/{{ owner.name }}</div>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
|
||||
<li v-if="ownersWithoutNamespace.length > 0">
|
||||
<details open>
|
||||
<summary class="text-base-content/80 font-light">
|
||||
<ph:circles-four />
|
||||
{{ $t("label.owners") }} ({{ ownersWithoutNamespace.length }})
|
||||
</summary>
|
||||
<ul>
|
||||
<li v-for="owner in ownersWithoutNamespace" :key="`${owner.kind}-${owner.name}`">
|
||||
<router-link :to="{ name: '/owner/[name]', params: { name: owner.name } }" active-class="menu-active">
|
||||
<ph:stack-simple />
|
||||
<div class="truncate">{{ owner.kind }}/{{ owner.name }}</div>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</SlideTransition>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const store = useK8sStore();
|
||||
|
||||
const { namespaces, owners } = storeToRefs(store);
|
||||
|
||||
const selectedNamespace = ref<string | null>("all");
|
||||
|
||||
const setNamespace = (namespace: string | null) => (selectedNamespace.value = namespace);
|
||||
|
||||
const filteredNamespaces = computed(() => {
|
||||
if (selectedNamespace.value === null || selectedNamespace.value === "all") {
|
||||
return namespaces.value;
|
||||
}
|
||||
return namespaces.value.filter((ns) => ns.name === selectedNamespace.value);
|
||||
});
|
||||
|
||||
const ownersWithoutNamespace = computed(() => {
|
||||
const filtered = owners.value.filter((owner) => !owner.namespace);
|
||||
if (selectedNamespace.value === null || selectedNamespace.value === "all") {
|
||||
return filtered;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const menu = useTemplateRef("menu");
|
||||
|
||||
const collapseAll = () => {
|
||||
const details = menu.value?.querySelectorAll("details");
|
||||
details?.forEach((detail) => (detail.open = false));
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<ScrollableView :scrollable="scrollable" v-if="namespace.name">
|
||||
<template #header>
|
||||
<div class="mx-2 flex items-center gap-2 md:ml-4">
|
||||
<div class="@container flex flex-1 items-center gap-1.5 md:gap-2">
|
||||
<ph:stack />
|
||||
<div class="font-mono text-sm font-semibold">{{ namespace.name }}</div>
|
||||
<ContainerDropdown :containers="namespace.containers">
|
||||
{{ $t("label.container", namespace.containers.length) }}
|
||||
</ContainerDropdown>
|
||||
</div>
|
||||
<MultiContainerStat class="ml-auto" :containers="namespace.containers" />
|
||||
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<ViewerWithSource
|
||||
ref="viewer"
|
||||
:stream-source="useNamespaceStream"
|
||||
:entity="namespace"
|
||||
:visible-keys="new Map<string[], boolean>()"
|
||||
/>
|
||||
</template>
|
||||
</ScrollableView>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { K8sNamespace } from "@/stores/k8s";
|
||||
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
|
||||
import { ComponentExposed } from "vue-component-type-helpers";
|
||||
|
||||
const { namespace, scrollable = false } = defineProps<{
|
||||
scrollable?: boolean;
|
||||
namespace: K8sNamespace;
|
||||
}>();
|
||||
|
||||
const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
|
||||
|
||||
provideLoggingContext(
|
||||
toRef(() => namespace.containers),
|
||||
{ showContainerName: true, showHostname: false },
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<ScrollableView :scrollable="scrollable" v-if="owner.name">
|
||||
<template #header>
|
||||
<div class="mx-2 flex items-center gap-2 md:ml-4">
|
||||
<div class="@container flex flex-1 items-center gap-1.5 md:gap-2">
|
||||
<ph:stack-simple />
|
||||
<div class="font-mono text-sm font-semibold">{{ owner.kind }}/{{ owner.name }}</div>
|
||||
<ContainerDropdown :containers="owner.containers">
|
||||
{{ $t("label.container", owner.containers.length) }}
|
||||
</ContainerDropdown>
|
||||
</div>
|
||||
<MultiContainerStat class="ml-auto" :containers="owner.containers" />
|
||||
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<ViewerWithSource
|
||||
ref="viewer"
|
||||
:stream-source="useOwnerStream"
|
||||
:entity="owner"
|
||||
:visible-keys="new Map<string[], boolean>()"
|
||||
/>
|
||||
</template>
|
||||
</ScrollableView>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { K8sOwner } from "@/stores/k8s";
|
||||
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
|
||||
import { ComponentExposed } from "vue-component-type-helpers";
|
||||
|
||||
const { owner, scrollable = false } = defineProps<{
|
||||
scrollable?: boolean;
|
||||
owner: K8sOwner;
|
||||
}>();
|
||||
|
||||
const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
|
||||
|
||||
provideLoggingContext(
|
||||
toRef(() => owner.containers),
|
||||
{ showContainerName: true, showHostname: false },
|
||||
);
|
||||
</script>
|
||||
+27
-29
@@ -1,20 +1,7 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<slot name="more-items"></slot>
|
||||
<Dropdown class="dropdown-end" @closed="latestTag = latest?.tag ?? config.version">
|
||||
<template #trigger>
|
||||
<mdi:announcement class="size-6 -rotate-12" />
|
||||
<span
|
||||
class="absolute right-px top-0 size-2 rounded-full bg-red"
|
||||
v-if="hasUpdate && latestTag != latest?.tag"
|
||||
></span>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="w-72">
|
||||
<Releases />
|
||||
</div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
<Announcements />
|
||||
|
||||
<router-link
|
||||
:to="{ name: '/settings' }"
|
||||
@@ -27,23 +14,31 @@
|
||||
|
||||
<dropdown class="dropdown-end" v-if="config.user">
|
||||
<template #trigger>
|
||||
<img
|
||||
class="size-6 max-w-none rounded-full p-px ring-1 ring-base-content/60"
|
||||
:src="withBase('/api/profile/avatar')"
|
||||
/>
|
||||
<template v-if="config.disableAvatars || !config.user.email">
|
||||
<material-symbols:person class="size-6" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<img
|
||||
class="ring-base-content/60 size-6 max-w-none rounded-full p-px ring-1"
|
||||
:src="withBase('/api/profile/avatar')"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="p-2">
|
||||
<div class="font-bold">
|
||||
{{ config.user.name }}
|
||||
</div>
|
||||
<div class="text-sm font-light">
|
||||
<div v-if="config.user.email" class="text-sm font-light">
|
||||
{{ config.user.email }}
|
||||
</div>
|
||||
</div>
|
||||
<ul class="menu mt-4 p-0">
|
||||
<li v-if="config.authProvider === 'simple'">
|
||||
<button @click.prevent="logout()" class="p-2 text-primary">{{ $t("button.logout") }}</button>
|
||||
<ul v-if="config.authProvider === 'simple' || config.logoutUrl" class="menu mt-4 p-0">
|
||||
<li>
|
||||
<button @click.prevent="logout()" class="text-primary p-2">
|
||||
<material-symbols:logout />
|
||||
{{ $t("button.logout") }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
@@ -51,14 +46,17 @@
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
const { logoutUrl } = config;
|
||||
|
||||
async function logout() {
|
||||
await fetch(withBase("/api/token"), {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (logoutUrl) {
|
||||
location.href = logoutUrl;
|
||||
} else {
|
||||
await fetch(withBase("/api/token"), {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
location.reload();
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
const { hasUpdate, latest } = useReleases();
|
||||
const latestTag = useProfileStorage("releaseSeen", config.version);
|
||||
</script>
|
||||
|
||||
@@ -1,55 +1,75 @@
|
||||
<template>
|
||||
<div class="group/item clickable relative flex w-full gap-x-2" @click="showDrawer(LogDetails, { entry: logEntry })">
|
||||
<div v-if="showContainerName">
|
||||
<ContainerName :id="logEntry.containerID" />
|
||||
<DefineTemplate v-slot="{ data }">
|
||||
<ul class="inline-flex flex-wrap space-x-4" @click="preventDefaultOnLinks">
|
||||
<li v-for="(value, name) in data" :key="name" v-if="isObject(data)">
|
||||
<span class="key">{{ name }}=</span>
|
||||
<span class="value" v-if="value === null"><null></span>
|
||||
<ReuseTemplate :data="value" v-else-if="isObject(value) || Array.isArray(value)" />
|
||||
<span v-else class="value" :class="typeof value" v-html="stripAnsi(value.toString())"></span>
|
||||
</li>
|
||||
<li v-else-if="Array.isArray(data)">
|
||||
<ul class="array inline-flex flex-wrap space-x-1">
|
||||
<li
|
||||
v-for="(item, index) in data"
|
||||
:key="index"
|
||||
class="after:text-base-content/70 not-last:after:content-[',']"
|
||||
>
|
||||
<ReuseTemplate :data="item" v-if="isObject(item) || Array.isArray(item)" />
|
||||
<span v-else class="value" :class="typeof item" v-html="stripAnsi(item.toString())"></span>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="key" v-if="Object.keys(validValues).length === 0">all values are hidden</li>
|
||||
</ul>
|
||||
</DefineTemplate>
|
||||
<LogItem :logEntry>
|
||||
<LogLevel class="flex select-none" :level="logEntry.level" />
|
||||
<div @click="containers.length > 0 && showDrawer(LogDetails, { entry: logEntry })" class="cursor-pointer">
|
||||
<ReuseTemplate :data="validValues" />
|
||||
</div>
|
||||
<div v-if="showStd">
|
||||
<LogStd :std="logEntry.std" />
|
||||
</div>
|
||||
<div v-if="showTimestamp">
|
||||
<LogDate :date="logEntry.date" class="select-none" />
|
||||
</div>
|
||||
<div class="flex">
|
||||
<LogLevel :level="logEntry.level" />
|
||||
</div>
|
||||
<div>
|
||||
<ul class="fields space-x-4">
|
||||
<li v-for="(value, name) in validValues" :key="name">
|
||||
<span class="text-light">{{ name }}=</span><span class="font-bold" v-if="value === null"><null></span>
|
||||
<template v-else-if="Array.isArray(value)">
|
||||
<span class="font-bold" v-html="JSON.stringify(value)"> </span>
|
||||
</template>
|
||||
<span class="font-bold" v-html="value" v-else></span>
|
||||
</li>
|
||||
<li class="text-light" v-if="Object.keys(validValues).length === 0">all values are hidden</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</LogItem>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import stripAnsi from "strip-ansi";
|
||||
import { type ComplexLogEntry } from "@/models/LogEntry";
|
||||
import LogDetails from "./LogDetails.vue";
|
||||
|
||||
const { logEntry, showContainerName = false } = defineProps<{
|
||||
const { logEntry } = defineProps<{
|
||||
logEntry: ComplexLogEntry;
|
||||
showContainerName?: boolean;
|
||||
}>();
|
||||
|
||||
const { containers } = useLoggingContext();
|
||||
|
||||
const [DefineTemplate, ReuseTemplate] = createReusableTemplate();
|
||||
|
||||
const validValues = computed(() => {
|
||||
return Object.fromEntries(Object.entries(logEntry.message).filter(([_, value]) => value !== undefined));
|
||||
});
|
||||
|
||||
const showDrawer = useDrawer();
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.text-light {
|
||||
@apply text-base-content/70;
|
||||
}
|
||||
|
||||
.fields {
|
||||
li {
|
||||
@apply inline-flex;
|
||||
function preventDefaultOnLinks(event: MouseEvent) {
|
||||
if (event.target instanceof HTMLAnchorElement && event.target.rel?.includes("external")) {
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/main.css";
|
||||
.key {
|
||||
@apply text-base-content/70 font-light;
|
||||
}
|
||||
|
||||
.value {
|
||||
@apply text-base-content font-bold;
|
||||
}
|
||||
|
||||
.array {
|
||||
@apply before:text-base-content/80 after:text-base-content/80 before:content-['['] after:content-[']'];
|
||||
}
|
||||
|
||||
.string {
|
||||
@apply before:content-['"'] after:content-['"'];
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,84 +1,19 @@
|
||||
<template>
|
||||
<div class="flex w-full flex-col">
|
||||
<div class="relative flex items-start gap-x-2">
|
||||
<ContainerName class="flex-none" :id="logEntry.containerID" v-if="showContainerName" />
|
||||
<LogDate :date="logEntry.date" v-if="showTimestamp" />
|
||||
<LogLevel class="flex" />
|
||||
<div class="whitespace-pre-wrap" :data-event="logEntry.event" v-html="logEntry.message"></div>
|
||||
</div>
|
||||
<div class="alert alert-info mt-8 w-auto flex-none font-sans text-[1rem] md:mx-auto md:w-1/2" v-if="followEligible">
|
||||
<carbon:information class="size-6 shrink-0 stroke-current" />
|
||||
<div>
|
||||
<h3 class="text-lg font-bold">{{ $t("alert.similar-container-found.title") }}</h3>
|
||||
{{ $t("alert.similar-container-found.message", { containerId: nextContainer.id }) }}
|
||||
</div>
|
||||
<div>
|
||||
<TimedButton v-if="automaticRedirect" class="btn-primary btn-sm" @finished="redirectNow()">{{
|
||||
$t("button.cancel")
|
||||
}}</TimedButton>
|
||||
<router-link
|
||||
:to="{ name: '/container/[id]', params: { id: nextContainer.id } }"
|
||||
class="btn btn-primary btn-sm"
|
||||
v-else
|
||||
>
|
||||
{{ $t("button.redirect") }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<LogItem :logEntry>
|
||||
<div class="whitespace-pre-wrap" :data-event="logEntry.event" v-html="logEntry.message"></div>
|
||||
</LogItem>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ContainerEventLogEntry } from "@/models/LogEntry";
|
||||
const router = useRouter();
|
||||
const { showToast } = useToast();
|
||||
const { t } = useI18n();
|
||||
|
||||
const { logEntry } = defineProps<{
|
||||
logEntry: ContainerEventLogEntry;
|
||||
showContainerName?: boolean;
|
||||
}>();
|
||||
|
||||
const { containers } = useLoggingContext();
|
||||
|
||||
const store = useContainerStore();
|
||||
|
||||
const { containers: allContainers } = storeToRefs(store);
|
||||
|
||||
const nextContainer = computed(
|
||||
() =>
|
||||
[
|
||||
...allContainers.value.filter(
|
||||
(c) =>
|
||||
c.host === containers.value[0].host &&
|
||||
c.created > logEntry.date &&
|
||||
c.name === containers.value[0].name &&
|
||||
c.state === "running",
|
||||
),
|
||||
].sort((a, b) => +a.created - +b.created)[0],
|
||||
);
|
||||
|
||||
const followEligible = computed(
|
||||
() =>
|
||||
router.currentRoute.value.name === "/container/[id]" &&
|
||||
logEntry.event === "container-stopped" &&
|
||||
containers.value.length === 1 &&
|
||||
nextContainer.value !== undefined,
|
||||
);
|
||||
|
||||
function redirectNow() {
|
||||
showToast(
|
||||
{
|
||||
title: t("alert.redirected.title"),
|
||||
message: t("alert.redirected.message", { containerId: nextContainer.value?.id }),
|
||||
type: "info",
|
||||
},
|
||||
{ expire: 5000 },
|
||||
);
|
||||
router.push({ name: "/container/[id]", params: { id: nextContainer.value?.id } });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
<style scoped>
|
||||
@reference "@/main.css";
|
||||
[data-event="container-stopped"] {
|
||||
@apply text-red;
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
<template>
|
||||
<div class="grid w-40 overflow-hidden rounded text-center text-sm text-white">
|
||||
<div class="random-color col-start-1 row-start-1 brightness-75"></div>
|
||||
<div class="col-start-1 row-start-1 truncate px-2 brightness-100 [direction:rtl]">{{ containerNames[id] }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
const colors = [
|
||||
"#4B0082",
|
||||
"#FF00FF",
|
||||
"#FF7F00",
|
||||
"#FFFF00",
|
||||
"#00FF00",
|
||||
"#00FFFF",
|
||||
"#FF0000",
|
||||
"#0000FF",
|
||||
"#FF007F",
|
||||
"#32CD32",
|
||||
"#40E0D0",
|
||||
"#E6E6FA",
|
||||
"#800080",
|
||||
"#FFD700",
|
||||
"#FF4040",
|
||||
] as const;
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
const containerStore = useContainerStore();
|
||||
const { containerNames } = storeToRefs(containerStore);
|
||||
|
||||
const { id } = defineProps<{
|
||||
id: string;
|
||||
}>();
|
||||
|
||||
const color = computed(() => colors[Math.abs(hashCode(id)) % colors.length]);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.random-color {
|
||||
background-color: v-bind(color);
|
||||
}
|
||||
</style>
|
||||
@@ -29,10 +29,15 @@ describe("<ContainerEventSource />", () => {
|
||||
global.EventSource = EventSource;
|
||||
// @ts-ignore
|
||||
window.scrollTo = vi.fn();
|
||||
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
|
||||
observe: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
}));
|
||||
global.IntersectionObserver = class IntersectionObserver {
|
||||
observe = vi.fn();
|
||||
disconnect = vi.fn();
|
||||
unobserve = vi.fn();
|
||||
takeRecords = vi.fn();
|
||||
root = null;
|
||||
rootMargin = "";
|
||||
thresholds = [];
|
||||
} as any;
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(1560336942459);
|
||||
});
|
||||
@@ -65,12 +70,29 @@ describe("<ContainerEventSource />", () => {
|
||||
template: "Test from createLogEventSource",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "/container/[id].time.[datetime]",
|
||||
path: "/container/:id/time/:datetime",
|
||||
component: {
|
||||
template: "Test from createLogEventSource",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return mount(Component, {
|
||||
global: {
|
||||
plugins: [router, createTestingPinia({ createSpy: vi.fn }), createI18n({})],
|
||||
plugins: [
|
||||
router,
|
||||
createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
stubActions: false,
|
||||
initialState: {
|
||||
container: { containers: [{ id: "abc", image: "test:v123", host: "localhost" }] },
|
||||
},
|
||||
}),
|
||||
createI18n({}),
|
||||
],
|
||||
components: {
|
||||
LogViewer,
|
||||
},
|
||||
@@ -84,6 +106,7 @@ describe("<ContainerEventSource />", () => {
|
||||
streamConfig: reactive({ stdout: true, stderr: true }),
|
||||
hasComplexLogs: ref(false),
|
||||
levels: new Set<Level>(["info"]),
|
||||
historical: ref(false),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -94,7 +117,21 @@ describe("<ContainerEventSource />", () => {
|
||||
},
|
||||
props: {
|
||||
streamSource: useContainerStream,
|
||||
entity: new Container("abc", new Date(), "image", "name", "command", "localhost", {}, "created", []),
|
||||
entity: new Container(
|
||||
"abc",
|
||||
new Date(), // created
|
||||
new Date(), // started
|
||||
new Date(), // finished
|
||||
"image",
|
||||
"name",
|
||||
"command",
|
||||
"localhost",
|
||||
{},
|
||||
"created",
|
||||
0,
|
||||
0,
|
||||
[],
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -103,7 +140,7 @@ describe("<ContainerEventSource />", () => {
|
||||
|
||||
test("renders loading correctly", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
expect(wrapper.find("ul.animate-pulse").exists()).toBe(true);
|
||||
});
|
||||
|
||||
test("should connect to EventSource", async () => {
|
||||
@@ -124,7 +161,7 @@ describe("<ContainerEventSource />", () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources[sourceUrl].emitOpen();
|
||||
sources[sourceUrl].emitMessage({
|
||||
data: `{"ts":1560336942459, "m":"This is a message.", "id":1}`,
|
||||
data: `{"ts":1560336942459, "m":"This is a message.", "id":1, "rm": "This is a message.", "c": "abc"}`,
|
||||
});
|
||||
|
||||
vi.runAllTimers();
|
||||
@@ -140,39 +177,39 @@ describe("<ContainerEventSource />", () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources[sourceUrl].emitOpen();
|
||||
sources[sourceUrl].emitMessage({
|
||||
data: `{"ts":1560336942459, "m":"This is a message.", "id":1}`,
|
||||
data: `{"ts":1560336942459, "m":"This is a message.", "id":1, "rm": "This is a message.", "c": "abc"}`,
|
||||
});
|
||||
|
||||
vi.runAllTimers();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
|
||||
expect(wrapper.find("ul[data-logs]").html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("should render dates with 12 hour style", async () => {
|
||||
const wrapper = createLogEventSource({ hourStyle: "12" });
|
||||
sources[sourceUrl].emitOpen();
|
||||
sources[sourceUrl].emitMessage({
|
||||
data: `{"ts":1560336942459, "m":"foo bar", "id":1}`,
|
||||
data: `{"ts":1560336942459, "m":"foo bar", "id":1, "rm": "foo bar", "c": "abc"}`,
|
||||
});
|
||||
|
||||
vi.runAllTimers();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
|
||||
expect(wrapper.find("ul[data-logs]").html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("should render dates with 24 hour style", async () => {
|
||||
const wrapper = createLogEventSource({ hourStyle: "24" });
|
||||
sources[sourceUrl].emitOpen();
|
||||
sources[sourceUrl].emitMessage({
|
||||
data: `{"ts":1560336942459, "m":"foo bar", "id":1}`,
|
||||
data: `{"ts":1560336942459, "m":"foo bar", "id":1, "c": "abc"}`,
|
||||
});
|
||||
|
||||
vi.runAllTimers();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
|
||||
expect(wrapper.find("ul[data-logs]").html()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,32 +1,76 @@
|
||||
<template>
|
||||
<InfiniteLoader :onLoadMore="fetchMore" :enabled="!loadingMore && messages.length > 10" />
|
||||
<slot :messages="messages"></slot>
|
||||
<ul class="flex animate-pulse flex-col gap-4 p-4" v-if="loading || (noLogs && waitingForMoreLog)">
|
||||
<div class="flex flex-row gap-2" v-for="size in sizes">
|
||||
<div class="bg-base-content/50 h-3 w-40 shrink-0 rounded-full opacity-50"></div>
|
||||
<div class="bg-base-content/50 h-3 rounded-full opacity-50" :class="size"></div>
|
||||
</div>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</ul>
|
||||
<div v-else-if="noLogs && !waitingForMoreLog" class="p-4">
|
||||
{{ $t("label.no-logs") }}
|
||||
</div>
|
||||
<slot :messages="messages" v-else></slot>
|
||||
<IndeterminateBar :color v-if="!historical" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup generic="T">
|
||||
import { LogStreamSource } from "@/composable/eventStreams";
|
||||
const route = useRoute();
|
||||
|
||||
const { entity, streamSource } = $defineProps<{
|
||||
streamSource: (t: Ref<T>) => LogStreamSource;
|
||||
entity: T;
|
||||
}>();
|
||||
|
||||
const { messages, loadOlderLogs, isLoadingMore } = streamSource($$(entity));
|
||||
const { loadingMore } = useLoggingContext();
|
||||
const { historical } = useLoggingContext();
|
||||
|
||||
const enabled = ref(true);
|
||||
const { messages, opened, loading, error } = streamSource(toRef(() => entity));
|
||||
|
||||
const color = computed(() => {
|
||||
if (error.value) return "error";
|
||||
if (loading.value) return "secondary";
|
||||
if (opened.value) return "primary";
|
||||
return "error";
|
||||
});
|
||||
|
||||
const noLogs = computed(() => messages.value.length === 0);
|
||||
const waitingForMoreLog = refAutoReset(false, 3000);
|
||||
watchImmediate(loading, () => (waitingForMoreLog.value = true));
|
||||
|
||||
defineExpose({
|
||||
clear: () => (messages.value = []),
|
||||
});
|
||||
|
||||
const fetchMore = async () => {
|
||||
if (!isLoadingMore.value) {
|
||||
loadingMore.value = true;
|
||||
enabled.value = false;
|
||||
await loadOlderLogs();
|
||||
loadingMore.value = false;
|
||||
enabled.value = true;
|
||||
}
|
||||
};
|
||||
if (historical.value && route.query.logId) {
|
||||
watchOnce(messages, async () => {
|
||||
await nextTick();
|
||||
document.getElementById(route.query.logId as string)?.scrollIntoView({ behavior: "instant", block: "center" });
|
||||
});
|
||||
}
|
||||
|
||||
const sizes = ref<string[]>([]);
|
||||
watch(
|
||||
opened,
|
||||
(value) => {
|
||||
if (value) return;
|
||||
const sizeOptions = [
|
||||
"w-2/12",
|
||||
"w-3/12",
|
||||
"w-4/12",
|
||||
"w-5/12",
|
||||
"w-6/12",
|
||||
"w-7/12",
|
||||
"w-8/12",
|
||||
"w-9/12",
|
||||
"w-10/12",
|
||||
"w-11/12",
|
||||
"w-full",
|
||||
];
|
||||
sizes.value = Array.from({ length: 18 }, () => sizeOptions[Math.floor(Math.random() * sizeOptions.length)]);
|
||||
},
|
||||
{
|
||||
flush: "sync",
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<LogItem :logEntry>
|
||||
<div class="flex flex-col">
|
||||
<div v-for="(msg, index) in logEntry.message" :key="index" class="flex items-start gap-x-2">
|
||||
<LogLevel class="flex select-none" :level="logEntry.level" :position="getPosition(index)" />
|
||||
<div
|
||||
class="[word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-pre"
|
||||
v-html="colorize(msg)"
|
||||
:class="{ 'min-h-4': msg === '' }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</LogItem>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { GroupedLogEntry, type Position } from "@/models/LogEntry";
|
||||
import AnsiConvertor from "ansi-to-html";
|
||||
|
||||
const ansiConvertor = new AnsiConvertor({
|
||||
escapeXML: false,
|
||||
fg: "var(--color-base-content)",
|
||||
bg: "var(--color-base-100)",
|
||||
});
|
||||
|
||||
const { logEntry } = defineProps<{
|
||||
logEntry: GroupedLogEntry;
|
||||
}>();
|
||||
|
||||
const getPosition = (index: number): Position => {
|
||||
const len = logEntry.message.length;
|
||||
if (index === 0) return "start";
|
||||
if (index === len - 1) return "end";
|
||||
return "middle";
|
||||
};
|
||||
|
||||
const colorize = (value: string) => ansiConvertor.toHtml(value);
|
||||
</script>
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div ref="root" class="flex min-h-[1px] flex-1 content-center justify-center">
|
||||
<span class="loading loading-bars loading-md text-primary m-2" v-show="isLoading"></span>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { LoadMoreLogEntry } from "@/models/LogEntry";
|
||||
|
||||
const { logEntry } = defineProps<{
|
||||
logEntry: LoadMoreLogEntry;
|
||||
}>();
|
||||
|
||||
const isLoading = ref(false);
|
||||
const root = ref<HTMLElement>();
|
||||
|
||||
useIntersectionObserver(root, async (entries) => {
|
||||
if (entries[0].intersectionRatio <= 0) return;
|
||||
if (isLoading.value) return;
|
||||
const scrollingParent = root.value?.closest("[data-scrolling]") || document.documentElement;
|
||||
const previousHeight = scrollingParent.scrollHeight;
|
||||
isLoading.value = true;
|
||||
await logEntry.loadMore();
|
||||
isLoading.value = false;
|
||||
await nextTick();
|
||||
if (logEntry.rememberScrollPosition) {
|
||||
scrollingParent.scrollTop += scrollingParent.scrollHeight - previousHeight;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<div
|
||||
class="dropdown dropdown-hover absolute -left-2 z-10 font-sans"
|
||||
:class="shouldShowBelow ? 'dropdown-right' : 'dropdown-right dropdown-end'"
|
||||
v-show="container"
|
||||
ref="dropdownRef"
|
||||
@mouseenter="checkDropdownPosition"
|
||||
>
|
||||
<router-link
|
||||
v-if="isSearching"
|
||||
@click="resetSearch()"
|
||||
tabindex="0"
|
||||
class="btn btn-square btn-xs border-base-content/20 bg-base-100 pointer-events-auto! opacity-0 shadow-sm group-hover/entry:opacity-90"
|
||||
:to="{
|
||||
name: '/container/[id].time.[datetime]',
|
||||
params: { id: container.id, datetime: logEntry.date.toISOString() },
|
||||
query: { logId: logEntry.id },
|
||||
}"
|
||||
>
|
||||
<material-symbols:eye-tracking />
|
||||
</router-link>
|
||||
<button
|
||||
tabindex="0"
|
||||
class="btn btn-square btn-xs border-base-content/20 bg-base-100 border opacity-0 shadow-sm group-hover/entry:opacity-90"
|
||||
v-else
|
||||
>
|
||||
<ion:ellipsis-vertical />
|
||||
</button>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 text-sm shadow-sm"
|
||||
@click="hideMenu"
|
||||
>
|
||||
<li v-if="isSearching">
|
||||
<router-link
|
||||
@click="resetSearch()"
|
||||
:to="{
|
||||
name: '/container/[id].time.[datetime]',
|
||||
params: { id: container.id, datetime: logEntry.date.toISOString() },
|
||||
query: { logId: logEntry.id },
|
||||
}"
|
||||
>
|
||||
<material-symbols:eye-tracking />
|
||||
{{ $t("action.see-in-context") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
@click="copyLogMessage()"
|
||||
:disabled="!isSupported"
|
||||
:title="!isSupported ? $t('error.copy-not-supported') : ''"
|
||||
:class="{ 'cursor-not-allowed opacity-50': !isSupported }"
|
||||
>
|
||||
<material-symbols:content-copy />
|
||||
{{ $t("action.copy-log") }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
@click="copyPermalink()"
|
||||
:disabled="!isSupported"
|
||||
:title="!isSupported ? $t('error.copy-not-supported') : ''"
|
||||
:class="{ 'cursor-not-allowed opacity-50': !isSupported }"
|
||||
>
|
||||
<material-symbols:link />
|
||||
{{ $t("action.copy-link") }}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li v-if="logEntry instanceof ComplexLogEntry">
|
||||
<a @click="showDrawer(LogDetails, { entry: logEntry })">
|
||||
<material-symbols:code-blocks-rounded />
|
||||
{{ $t("action.show-details") }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import stripAnsi from "strip-ansi";
|
||||
import { Container } from "@/models/Container";
|
||||
import { LogEntry, SimpleLogEntry, ComplexLogEntry, GroupedLogEntry, JSONObject } from "@/models/LogEntry";
|
||||
import LogDetails from "./LogDetails.vue";
|
||||
|
||||
const { logEntry, container } = defineProps<{
|
||||
logEntry: LogEntry<string | JSONObject>;
|
||||
container: Container;
|
||||
}>();
|
||||
|
||||
const { showToast } = useToast();
|
||||
const showDrawer = useDrawer();
|
||||
const router = useRouter();
|
||||
const { isSearching, resetSearch } = useSearchFilter();
|
||||
|
||||
const { copy, isSupported, copied } = useClipboard();
|
||||
const { t } = useI18n();
|
||||
|
||||
async function copyLogMessage() {
|
||||
if (!isSupported.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (logEntry instanceof ComplexLogEntry) {
|
||||
await copy(stripAnsi(logEntry.rawMessage));
|
||||
} else if (logEntry instanceof SimpleLogEntry) {
|
||||
await copy(stripAnsi(logEntry.rawMessage));
|
||||
} else if (logEntry instanceof GroupedLogEntry) {
|
||||
await copy(stripAnsi(logEntry.message.join("\n")));
|
||||
}
|
||||
|
||||
if (copied.value) {
|
||||
showToast(
|
||||
{
|
||||
title: t("toasts.copied.title"),
|
||||
message: t("toasts.copied.message"),
|
||||
type: "info",
|
||||
},
|
||||
{ expire: 2000 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function copyPermalink() {
|
||||
if (!isSupported.value) {
|
||||
return;
|
||||
}
|
||||
const url = router.resolve({
|
||||
name: "/container/[id].time.[datetime]",
|
||||
params: { id: container.id, datetime: logEntry.date.toISOString() },
|
||||
query: { logId: logEntry.id },
|
||||
}).href;
|
||||
|
||||
const resolved = new URL(url, window.location.origin);
|
||||
|
||||
await copy(resolved.href);
|
||||
|
||||
if (copied.value) {
|
||||
showToast(
|
||||
{
|
||||
title: t("toasts.copied.title"),
|
||||
message: t("toasts.copied.message"),
|
||||
type: "info",
|
||||
},
|
||||
{ expire: 2000 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function hideMenu(e: MouseEvent) {
|
||||
if (e.target instanceof HTMLAnchorElement) {
|
||||
setTimeout(() => {
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
const dropdownRef = useTemplateRef<HTMLDivElement>("dropdownRef");
|
||||
const shouldShowBelow = ref(false);
|
||||
|
||||
function checkDropdownPosition() {
|
||||
if (!dropdownRef.value) return;
|
||||
|
||||
const rect = dropdownRef.value.getBoundingClientRect();
|
||||
shouldShowBelow.value = rect.top < 150;
|
||||
}
|
||||
</script>
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<aside>
|
||||
<header class="flex items-center gap-4">
|
||||
<h1 class="mobile-hidden text-2xl">{{ container.name }}</h1>
|
||||
<h2 class="text-sm"><DistanceTime :date="container.created" /></h2>
|
||||
<h1 class="text-2xl max-md:hidden">{{ container.name }}</h1>
|
||||
<h2 class="text-sm"><RelativeTime :date="container.created" /></h2>
|
||||
</header>
|
||||
|
||||
<div class="mt-8 flex flex-col gap-2">
|
||||
@@ -11,18 +11,26 @@
|
||||
<textarea
|
||||
v-model="query"
|
||||
class="textarea textarea-primary w-full font-mono text-lg"
|
||||
:class="{ 'textarea-error': error }"
|
||||
:class="{ 'textarea-error!': error }"
|
||||
:disabled="state === 'downloading'"
|
||||
></textarea>
|
||||
<div class="label">
|
||||
<span class="label-text-alt text-error" v-if="error">{{ error }}</span>
|
||||
<span class="label-text-alt" v-else>
|
||||
Total {{ results.numRows }} records
|
||||
<template v-if="results.numRows > pageLimit">. Showing first {{ page.numRows }}.</template></span
|
||||
>
|
||||
<div class="mt-2">
|
||||
<span class="text-error" v-if="error">{{ error }}</span>
|
||||
<span v-else-if="state === 'initializing'">{{ $t("analytics.creating_table") }}</span>
|
||||
<span v-else-if="state === 'downloading'">{{
|
||||
$t("analytics.downloading", { size: formatBytes(bytes, { decimals: 1 }) })
|
||||
}}</span>
|
||||
<span v-else-if="evaluating">{{ $t("analytics.evaluating_query") }}</span>
|
||||
<span v-else>
|
||||
{{ $t("analytics.total_records", { count: results.numRows.toLocaleString() }) }}
|
||||
<template v-if="results.numRows > pageLimit">{{
|
||||
$t("analytics.showing_first", { count: page.numRows.toLocaleString() })
|
||||
}}</template>
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</section>
|
||||
<SQLTable :table="page" :loading="evaluating || !isReady" />
|
||||
<SQLTable :table="page" :loading="evaluating || state !== 'ready'" />
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
@@ -30,12 +38,15 @@
|
||||
<script setup lang="ts">
|
||||
import { Container } from "@/models/Container";
|
||||
import { type Table } from "@apache-arrow/esnext-esm";
|
||||
|
||||
const { container } = defineProps<{ container: Container }>();
|
||||
const query = ref("SELECT * FROM logs LIMIT 100");
|
||||
const error = ref<string | null>(null);
|
||||
const debouncedQuery = debouncedRef(query, 500);
|
||||
const evaluating = ref(false);
|
||||
const pageLimit = 1000;
|
||||
const state = ref<"downloading" | "ready" | "initializing">("downloading");
|
||||
const bytes = ref(0);
|
||||
|
||||
const url = withBase(
|
||||
`/api/hosts/${container.host}/containers/${container.id}/logs?stdout=1&stderr=1&everything&jsonOnly`,
|
||||
@@ -51,27 +62,49 @@ if (!response.ok) {
|
||||
const { db, conn } = await useDuckDB();
|
||||
const empty = await conn.query<Record<string, any>>(`SELECT 1 LIMIT 0`);
|
||||
|
||||
const { isReady } = useAsyncState(
|
||||
async () => {
|
||||
await db.registerFileBuffer("logs.json", new Uint8Array(await response.arrayBuffer()));
|
||||
onMounted(async () => {
|
||||
try {
|
||||
state.value = "downloading";
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) throw new Error("No reader available from stream");
|
||||
|
||||
const chunks: Uint8Array[] = [];
|
||||
bytes.value = 0;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
chunks.push(value);
|
||||
bytes.value += value.length;
|
||||
}
|
||||
|
||||
const arrayBuffer = new Uint8Array(bytes.value);
|
||||
let position = 0;
|
||||
for (const chunk of chunks) {
|
||||
arrayBuffer.set(chunk, position);
|
||||
position += chunk.length;
|
||||
}
|
||||
|
||||
await db.registerFileBuffer("logs.json", arrayBuffer);
|
||||
|
||||
state.value = "initializing";
|
||||
await conn.query(
|
||||
`CREATE TABLE logs AS SELECT unnest(m) FROM read_json('logs.json', ignore_errors = true, format = 'newline_delimited')`,
|
||||
);
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
onError: (e) => {
|
||||
console.error(e);
|
||||
if (e instanceof Error) {
|
||||
error.value = e.message;
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
state.value = "ready";
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e instanceof Error) {
|
||||
error.value = e.message;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const results = computedAsync(
|
||||
async () => {
|
||||
if (isReady.value) {
|
||||
if (state.value === "ready") {
|
||||
return await conn.query<Record<string, any>>(debouncedQuery.value);
|
||||
} else {
|
||||
return empty;
|
||||
@@ -89,9 +122,12 @@ const results = computedAsync(
|
||||
},
|
||||
);
|
||||
|
||||
whenever(evaluating, () => (error.value = null));
|
||||
whenever(evaluating, () => {
|
||||
error.value = null;
|
||||
state.value = "ready";
|
||||
});
|
||||
const page = computed(() =>
|
||||
results.value.numRows > pageLimit ? results.value.slice(0, pageLimit) : results.value,
|
||||
) as unknown as ComputedRef<Table<Record<string, any>>>;
|
||||
</script>
|
||||
<style lang="postcss" scoped></style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Tag size="small">
|
||||
<DateTime :date="date" class="whitespace-nowrap text-blue" />
|
||||
<Tag class="items-start!">
|
||||
<DateTime :date="date" class="text-blue whitespace-nowrap" />
|
||||
</Tag>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<header class="flex items-center gap-4">
|
||||
<Tag :data-level="entry.level" class="uppercase text-white" v-if="entry.level">{{ entry.level }}</Tag>
|
||||
<h1 class="mobile-hidden text-lg">
|
||||
<Tag :data-level="entry.level" class="show-unknown text-white uppercase" v-if="entry.level">{{ entry.level }}</Tag>
|
||||
<h1 class="text-lg max-md:hidden">
|
||||
<DateTime :date="entry.date" />
|
||||
</h1>
|
||||
<h2 class="text-sm"><DistanceTime :date="entry.date" /> on {{ entry.std }}</h2>
|
||||
<h2 class="text-sm"><RelativeTime :date="entry.date" /> on {{ entry.std }}</h2>
|
||||
</header>
|
||||
|
||||
<div class="mt-8 flex flex-col gap-10">
|
||||
@@ -26,28 +26,28 @@
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col gap-2">
|
||||
<div class="flex gap-2 font-thin">
|
||||
<div class="flex gap-2">
|
||||
Raw JSON
|
||||
|
||||
<UseClipboard v-slot="{ copy, copied }" :source="JSON.stringify(entry.unfilteredMessage)">
|
||||
<button class="swap swap-flip outline-none" @click="copy()" :class="{ 'hover:swap-active': copied }">
|
||||
<UseClipboard v-slot="{ copy, copied }" :source="entry.rawMessage">
|
||||
<button class="swap outline-hidden" @click="copy()" :class="{ 'hover:swap-active': copied }">
|
||||
<mdi:check class="swap-on" />
|
||||
<mdi:content-copy class="swap-off" />
|
||||
<material-symbols:content-copy class="swap-off" />
|
||||
</button>
|
||||
</UseClipboard>
|
||||
</div>
|
||||
<div class="max-h-48 overflow-scroll rounded border border-base-lighter bg-base-darker p-2">
|
||||
<pre v-html="syntaxHighlight(entry.unfilteredMessage)"></pre>
|
||||
<div class="bg-base-200 max-h-48 overflow-scroll rounded-sm border border-white/20 p-2">
|
||||
<pre v-html="syntaxHighlight(entry.rawMessage)"></pre>
|
||||
</div>
|
||||
</section>
|
||||
<table class="table table-pin-rows table-fixed" v-if="entry instanceof ComplexLogEntry">
|
||||
<table class="table-pin-rows table table-fixed" v-if="entry instanceof ComplexLogEntry">
|
||||
<caption class="caption-bottom">
|
||||
Fields are sortable by dragging and dropping.
|
||||
</caption>
|
||||
<thead class="text-lg">
|
||||
<tr>
|
||||
<th class="w-60">Field</th>
|
||||
<th class="mobile-hidden">Value</th>
|
||||
<th class="max-md:hidden">Value</th>
|
||||
<th class="w-20">
|
||||
<input type="checkbox" class="toggle toggle-primary" v-model="toggleAllFields" title="Toggle all" />
|
||||
</th>
|
||||
@@ -55,11 +55,11 @@
|
||||
</thead>
|
||||
<tbody ref="list">
|
||||
<tr v-for="{ key, value, enabled } in fields" :key="key.join('.')" class="hover">
|
||||
<td class="cursor-move break-all font-mono">
|
||||
<td class="cursor-move font-mono break-all">
|
||||
{{ key.join(".") }}
|
||||
</td>
|
||||
<td class="mobile-hidden truncate">
|
||||
<code v-html="JSON.stringify(value)"></code>
|
||||
<td class="truncate max-md:hidden">
|
||||
<code>{{ JSON.stringify(value) }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" class="toggle toggle-primary" :checked="enabled" @change="toggleField(key)" />
|
||||
@@ -72,7 +72,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ComplexLogEntry } from "@/models/LogEntry";
|
||||
import { useSortable } from "@vueuse/integrations/useSortable";
|
||||
import { UseClipboard } from "@vueuse/components";
|
||||
|
||||
const { entry } = defineProps<{ entry: ComplexLogEntry }>();
|
||||
@@ -82,6 +81,8 @@ const container = currentContainer(toRef(() => entry.containerID));
|
||||
const visibleKeys = persistentVisibleKeysForContainer(container);
|
||||
const { hosts } = useHosts();
|
||||
|
||||
const { useSortable } = await import("@vueuse/integrations/useSortable");
|
||||
|
||||
function toggleField(key: string[]) {
|
||||
if (visibleKeys.value.size === 0) {
|
||||
visibleKeys.value = new Map<string[], boolean>(fields.value.map(({ key }) => [key, true]));
|
||||
@@ -95,14 +96,15 @@ function toggleField(key: string[]) {
|
||||
const fields = computed({
|
||||
get() {
|
||||
const fieldsWithValue: { key: string[]; value: any; enabled: boolean }[] = [];
|
||||
const allFields = flattenJSONToMap(entry.unfilteredMessage);
|
||||
const rawFields = JSON.parse(entry.rawMessage);
|
||||
const allFields = flattenJSONToMap(rawFields);
|
||||
if (visibleKeys.value.size === 0) {
|
||||
for (const [key, value] of allFields) {
|
||||
fieldsWithValue.push({ key, value, enabled: true });
|
||||
}
|
||||
} else {
|
||||
for (const [key, enabled] of visibleKeys.value) {
|
||||
const value = getDeep(entry.unfilteredMessage, key);
|
||||
const value = getDeep(rawFields, key);
|
||||
fieldsWithValue.push({ key, value, enabled });
|
||||
}
|
||||
|
||||
@@ -140,14 +142,14 @@ const toggleAllFields = computed({
|
||||
},
|
||||
});
|
||||
|
||||
function syntaxHighlight(json: any) {
|
||||
json = JSON.stringify(json, null, 2);
|
||||
function syntaxHighlight(json: string) {
|
||||
json = JSON.stringify(JSON.parse(json.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")), null, 2);
|
||||
return json.replace(
|
||||
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|\b\d+\b)/g,
|
||||
function (match: string) {
|
||||
var cls = "json-number";
|
||||
if (/^"/.test(match)) {
|
||||
if (/:$/.test(match)) {
|
||||
if (match.startsWith('"')) {
|
||||
if (match.endsWith(":")) {
|
||||
cls = "json-key";
|
||||
} else {
|
||||
cls = "json-string";
|
||||
@@ -164,7 +166,8 @@ function syntaxHighlight(json: any) {
|
||||
|
||||
useSortable(list, fields);
|
||||
</script>
|
||||
<style lang="postcss" scoped>
|
||||
<style scoped>
|
||||
@reference "@/main.css";
|
||||
.font-mono {
|
||||
font-family:
|
||||
ui-monospace,
|
||||
@@ -178,19 +181,19 @@ useSortable(list, fields);
|
||||
}
|
||||
|
||||
pre {
|
||||
:deep(.json-key) {
|
||||
& :deep(.json-key) {
|
||||
@apply text-blue;
|
||||
}
|
||||
:deep(.json-string) {
|
||||
& :deep(.json-string) {
|
||||
@apply text-green;
|
||||
}
|
||||
:deep(.json-number) {
|
||||
& :deep(.json-number) {
|
||||
@apply text-orange;
|
||||
}
|
||||
:deep(.json-boolean) {
|
||||
& :deep(.json-boolean) {
|
||||
@apply text-purple;
|
||||
}
|
||||
:deep(.json-null) {
|
||||
& :deep(.json-null) {
|
||||
@apply text-red;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
|
||||
<LogActions :logEntry :container />
|
||||
|
||||
<LogStd :std="logEntry.std" class="shrink-0 select-none" v-if="showStd" />
|
||||
|
||||
<div class="flex gap-x-2 gap-y-1 group-[.compact]:gap-y-0 has-[>_*:nth-of-type(2)]:flex-col-reverse md:flex-row!">
|
||||
<RandomColorTag class="w-30 shrink-0 select-none md:w-40" :value="host.name" v-if="showHostname" />
|
||||
<RandomColorTag
|
||||
v-if="showContainerName"
|
||||
class="w-30 shrink-0 select-none group-[.compact]:flex-1 md:w-40"
|
||||
:value="container.name"
|
||||
truncateRight
|
||||
/>
|
||||
<LogDate
|
||||
v-if="showTimestamp"
|
||||
:date="logEntry.date"
|
||||
class="shrink-0 select-none"
|
||||
:class="{ 'bg-secondary': route.query.logId === logEntry.id.toString() }"
|
||||
/>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { LogEntry } from "@/models/LogEntry";
|
||||
|
||||
const { logEntry } = defineProps<{
|
||||
logEntry: LogEntry<any>;
|
||||
}>();
|
||||
const { showHostname, showContainerName } = useLoggingContext();
|
||||
|
||||
const { currentContainer } = useContainerStore();
|
||||
const { hosts } = useHosts();
|
||||
|
||||
const container = currentContainer(toRef(() => logEntry.containerID));
|
||||
const host = computed(() => hosts.value[container.value.host]);
|
||||
|
||||
const route = useRoute();
|
||||
</script>
|
||||
@@ -1,39 +1,50 @@
|
||||
<template>
|
||||
<div :data-level="level" :data-position="position" class="mt-1.5 size-2.5 flex-none rounded-lg"></div>
|
||||
<div
|
||||
:data-level="level"
|
||||
:data-position="position"
|
||||
class="mt-1.5 size-2.5 flex-none rounded-lg"
|
||||
:class="{ showUnknown }"
|
||||
></div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { Position } from "@/models/LogEntry";
|
||||
import { Position, Level } from "@/models/LogEntry";
|
||||
|
||||
defineProps<{
|
||||
level?: string;
|
||||
const {
|
||||
level,
|
||||
position,
|
||||
showUnknown = false,
|
||||
} = defineProps<{
|
||||
level?: Level;
|
||||
position?: Position;
|
||||
showUnknown?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
<style scoped>
|
||||
@reference "@/main.css";
|
||||
[data-position="start"],
|
||||
[data-position="middle"],
|
||||
[data-position="end"] {
|
||||
align-self: stretch;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[data-position="start"] {
|
||||
border-radius: 0.5em 0.5em 0 0;
|
||||
height: 70%;
|
||||
margin-bottom: -0.4em;
|
||||
margin-top: auto;
|
||||
align-self: flex-end;
|
||||
border-radius: 0.375rem 0.375rem 0 0;
|
||||
}
|
||||
|
||||
[data-position="middle"] {
|
||||
border-radius: 0;
|
||||
height: auto;
|
||||
margin: -0.4em 0;
|
||||
align-self: stretch;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
[data-position="end"] {
|
||||
border-radius: 0 0 0.5em 0.5em;
|
||||
height: 70%;
|
||||
margin-top: -0.4em;
|
||||
align-self: flex-start;
|
||||
border-radius: 0 0 0.375rem 0.375rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
<style lang="postcss">
|
||||
<style>
|
||||
@reference "@/main.css";
|
||||
[data-level="debug"],
|
||||
[data-level="trace"] {
|
||||
@apply !bg-purple;
|
||||
@@ -44,14 +55,15 @@ defineProps<{
|
||||
}
|
||||
|
||||
[data-level="error"],
|
||||
[data-level="severe"],
|
||||
[data-level="critical"],
|
||||
[data-level="fatal"] {
|
||||
@apply !bg-red;
|
||||
}
|
||||
|
||||
[data-level="warn"],
|
||||
[data-level="warning"] {
|
||||
[data-level="warn"] {
|
||||
@apply !bg-orange;
|
||||
}
|
||||
|
||||
[data-level="unknown"].show-unknown {
|
||||
@apply !bg-base-300;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
<template>
|
||||
<ul
|
||||
class="events group pt-4"
|
||||
:class="{ 'disable-wrap': !softWrap, [size]: true, compact }"
|
||||
v-if="messages.length > 0"
|
||||
>
|
||||
<ul class="group pt-4" :class="{ 'disable-wrap': !softWrap, [size]: true, compact }" data-logs>
|
||||
<li
|
||||
v-for="item in messages"
|
||||
ref="list"
|
||||
:key="item.id"
|
||||
:data-key="item.id"
|
||||
:id="item.id.toString()"
|
||||
:data-time="item.date.getTime()"
|
||||
class="group/entry"
|
||||
>
|
||||
<component :is="item.getComponent()" :log-entry="item" :show-container-name="showContainerName" />
|
||||
<component :is="item.getComponent()" :log-entry="item" />
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
@@ -20,21 +16,17 @@
|
||||
<script lang="ts" setup>
|
||||
import { type JSONObject, LogEntry } from "@/models/LogEntry";
|
||||
|
||||
const { loading, progress, currentDate } = useScrollContext();
|
||||
const { progress, currentDate } = useScrollContext();
|
||||
|
||||
const { messages } = defineProps<{
|
||||
messages: LogEntry<string | JSONObject>[];
|
||||
showContainerName: boolean;
|
||||
messages: LogEntry<string | string[] | JSONObject>[];
|
||||
}>();
|
||||
|
||||
watchEffect(() => {
|
||||
loading.value = messages.length === 0;
|
||||
});
|
||||
|
||||
const { containers } = useLoggingContext();
|
||||
|
||||
const list = ref<HTMLElement[]>([]);
|
||||
|
||||
let previousDate = new Date();
|
||||
useIntersectionObserver(
|
||||
list,
|
||||
(entries) => {
|
||||
@@ -45,9 +37,12 @@ useIntersectionObserver(
|
||||
const time = entry.target.getAttribute("data-time");
|
||||
if (time) {
|
||||
const date = new Date(parseInt(time));
|
||||
if (+date === +previousDate) break;
|
||||
previousDate = date;
|
||||
const diff = new Date().getTime() - container.created.getTime();
|
||||
progress.value = (date.getTime() - container.created.getTime()) / diff;
|
||||
currentDate.value = date;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,8 +53,9 @@ useIntersectionObserver(
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<style scoped lang="postcss">
|
||||
.events {
|
||||
<style scoped>
|
||||
@reference "@/main.css";
|
||||
ul {
|
||||
font-family:
|
||||
ui-monospace,
|
||||
SFMono-Regular,
|
||||
@@ -71,14 +67,10 @@ useIntersectionObserver(
|
||||
monospace;
|
||||
|
||||
> li {
|
||||
@apply flex break-words px-2 py-1 last:snap-end odd:bg-gray-400/[0.07] has-[.clickable]:cursor-pointer has-[.clickable]:hover:bg-primary/10 md:px-4;
|
||||
@apply flex px-2 py-1 break-words last:snap-end odd:bg-gray-400/[0.07] md:px-4;
|
||||
&:last-child {
|
||||
scroll-margin-block-end: 5rem;
|
||||
}
|
||||
|
||||
.jump-context {
|
||||
@apply mr-2 flex items-center font-sans text-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
&.small {
|
||||
@@ -90,7 +82,7 @@ useIntersectionObserver(
|
||||
}
|
||||
|
||||
&.large {
|
||||
@apply text-lg;
|
||||
@apply text-[1em];
|
||||
}
|
||||
|
||||
&.compact {
|
||||
@@ -102,5 +94,23 @@ useIntersectionObserver(
|
||||
@apply rounded-none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(mark) {
|
||||
@apply bg-secondary inline-block rounded-xs;
|
||||
animation: pops 200ms ease-out;
|
||||
}
|
||||
|
||||
:deep(a[rel~="external"]) {
|
||||
@apply text-primary underline-offset-4 hover:underline;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pops {
|
||||
0% {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<template>
|
||||
<div class="flex gap-2">
|
||||
<div
|
||||
class="flex min-w-[0.98rem] items-start justify-end align-bottom hover:cursor-pointer"
|
||||
v-if="isSupported"
|
||||
:title="t('log_actions.copy_log')"
|
||||
>
|
||||
<span
|
||||
class="rounded bg-slate-800/60 px-1.5 py-1 text-primary hover:bg-slate-700"
|
||||
@click.prevent="copyLogMessageToClipBoard()"
|
||||
>
|
||||
<carbon:copy-file />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { LogEntry, JSONObject } from "@/models/LogEntry";
|
||||
|
||||
const { message } = defineProps<{
|
||||
message: () => string;
|
||||
logEntry: LogEntry<string | JSONObject>;
|
||||
}>();
|
||||
|
||||
const { showToast } = useToast();
|
||||
const { copy, isSupported, copied } = useClipboard();
|
||||
const { t } = useI18n();
|
||||
|
||||
async function copyLogMessageToClipBoard() {
|
||||
await copy(message());
|
||||
|
||||
if (copied.value) {
|
||||
showToast(
|
||||
{
|
||||
title: t("toasts.copied.title"),
|
||||
message: t("toasts.copied.message"),
|
||||
type: "info",
|
||||
},
|
||||
{ expire: 2000 },
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Tag size="small" :std="std">
|
||||
<Tag :std="std" class="items-start!">
|
||||
{{ std }}
|
||||
</Tag>
|
||||
</template>
|
||||
@@ -12,7 +12,8 @@ defineProps<{
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
<style scoped>
|
||||
@reference "@/main.css";
|
||||
[std="stdout"] {
|
||||
@apply text-blue;
|
||||
}
|
||||
|
||||
@@ -1,43 +1,18 @@
|
||||
<template>
|
||||
<LogList :messages="visibleMessages" :show-container-name="showContainerName" />
|
||||
<LogList :messages="visibleMessages" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type JSONObject, LogEntry } from "@/models/LogEntry";
|
||||
|
||||
const props = defineProps<{
|
||||
messages: LogEntry<string | JSONObject>[];
|
||||
messages: LogEntry<string | string[] | JSONObject>[];
|
||||
visibleKeys: Map<string[], boolean>;
|
||||
showContainerName: boolean;
|
||||
}>();
|
||||
|
||||
const { messages, visibleKeys } = toRefs(props);
|
||||
|
||||
const { filteredPayload } = useVisibleFilter(visibleKeys);
|
||||
const { debouncedSearchFilter } = useSearchFilter();
|
||||
const { streamConfig } = useLoggingContext();
|
||||
|
||||
const visibleMessages = filteredPayload(messages);
|
||||
const router = useRouter();
|
||||
|
||||
watchEffect(() => {
|
||||
const query = {} as Record<string, string>;
|
||||
if (debouncedSearchFilter.value !== "") {
|
||||
query.search = debouncedSearchFilter.value;
|
||||
}
|
||||
|
||||
if (!streamConfig.value.stderr) {
|
||||
query.stderr = streamConfig.value.stderr.toString();
|
||||
}
|
||||
|
||||
if (!streamConfig.value.stdout) {
|
||||
query.stdout = streamConfig.value.stdout.toString();
|
||||
}
|
||||
|
||||
router.push({
|
||||
query,
|
||||
replace: true,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="postcss"></style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
<template>
|
||||
<div class="dropdown dropdown-end dropdown-hover">
|
||||
<label tabindex="0" class="btn btn-ghost btn-sm gap-0.5 px-2">
|
||||
<carbon:circle-solid class="w-2.5 text-red" v-if="streamConfig.stderr" />
|
||||
<carbon:circle-solid class="w-2.5 text-blue" v-if="streamConfig.stdout" />
|
||||
<div class="dropdown dropdown-end dropdown-hover z-20">
|
||||
<label tabindex="0" class="btn btn-ghost btn-sm w-8 gap-0 px-0 md:gap-0.5">
|
||||
<carbon:circle-solid class="text-red w-2 md:w-2.5" v-if="streamConfig.stderr" />
|
||||
<carbon:circle-solid class="text-blue w-2 md:w-2.5" v-if="streamConfig.stdout" />
|
||||
</label>
|
||||
<ul tabindex="0" class="menu dropdown-content z-50 w-52 rounded-box bg-base p-1 shadow">
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 shadow-sm"
|
||||
@click="hideMenu"
|
||||
>
|
||||
<li>
|
||||
<a @click.prevent="clear()">
|
||||
<a @click="clear()">
|
||||
<octicon:trash-24 /> {{ $t("toolbar.clear") }}
|
||||
<KeyShortcut char="k" :modifiers="['shift', 'meta']" />
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="enableDownload">
|
||||
<a :href="downloadUrl" download>
|
||||
<octicon:download-24 />
|
||||
{{ isFiltered ? $t("toolbar.download-filtered") : $t("toolbar.download") }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a @click.prevent="showSearch = true">
|
||||
<a @click="showSearch = true">
|
||||
<mdi:magnify /> {{ $t("toolbar.search") }}
|
||||
<KeyShortcut char="f" />
|
||||
</a>
|
||||
@@ -27,8 +37,8 @@
|
||||
>
|
||||
<div class="flex size-4 gap-0.5">
|
||||
<template v-if="streamConfig.stderr && streamConfig.stdout">
|
||||
<carbon:circle-solid class="w-2 text-red" />
|
||||
<carbon:circle-solid class="w-2 text-blue" />
|
||||
<carbon:circle-solid class="text-red w-2" />
|
||||
<carbon:circle-solid class="text-blue w-2" />
|
||||
</template>
|
||||
</div>
|
||||
{{ $t("toolbar.show-all") }}
|
||||
@@ -42,7 +52,7 @@
|
||||
"
|
||||
>
|
||||
<div class="flex size-4 flex-col gap-1">
|
||||
<carbon:circle-solid class="w-2 text-blue" v-if="!streamConfig.stderr && streamConfig.stdout" />
|
||||
<carbon:circle-solid class="text-blue w-2" v-if="!streamConfig.stderr && streamConfig.stdout" />
|
||||
</div>
|
||||
{{ $t("toolbar.show", { std: "STDOUT" }) }}
|
||||
</a>
|
||||
@@ -55,26 +65,54 @@
|
||||
"
|
||||
>
|
||||
<div class="flex size-4 flex-col gap-1">
|
||||
<carbon:circle-solid class="w-2 text-red" v-if="streamConfig.stderr && !streamConfig.stdout" />
|
||||
<carbon:circle-solid class="text-red w-2" v-if="streamConfig.stderr && !streamConfig.stdout" />
|
||||
</div>
|
||||
{{ $t("toolbar.show", { std: "STDERR" }) }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="line"></li>
|
||||
<li>
|
||||
<a @click="showHostname = !showHostname">
|
||||
<mdi:check class="w-4" v-if="showHostname" />
|
||||
<div v-else class="w-4"></div>
|
||||
{{ $t("toolbar.show-hostname") }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a @click="showContainerName = !showContainerName">
|
||||
<mdi:check class="w-4" v-if="showContainerName" />
|
||||
<div v-else class="w-4"></div>
|
||||
{{ $t("toolbar.show-container-name") }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const { showSearch } = useSearchFilter();
|
||||
|
||||
const { enableDownload } = config;
|
||||
const clear = defineEmit();
|
||||
|
||||
const { streamConfig } = useLoggingContext();
|
||||
const { streamConfig, showHostname, showContainerName, containers, levels } = useLoggingContext();
|
||||
|
||||
const { downloadUrl, isFiltered } = useDownloadUrl(containers, streamConfig, levels);
|
||||
|
||||
const hideMenu = (e: MouseEvent) => {
|
||||
if (e.target instanceof HTMLAnchorElement) {
|
||||
setTimeout(() => {
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
<style scoped>
|
||||
@reference "@/main.css";
|
||||
li.line {
|
||||
@apply h-px bg-base-content/20;
|
||||
@apply bg-base-content/20 h-px;
|
||||
}
|
||||
|
||||
a {
|
||||
|
||||
@@ -1,20 +1,62 @@
|
||||
<template>
|
||||
<div class="flex gap-4">
|
||||
<StatMonitor :data="memoryData" label="mem" :stat-value="formatBytes(totalStat.memoryUsage)" />
|
||||
<StatMonitor :data="cpuData" label="load" :stat-value="Math.max(0, totalStat.cpu).toFixed(2) + '%'" />
|
||||
<div class="flex gap-1 md:gap-4">
|
||||
<div
|
||||
class="grid min-w-15 grid-cols-[auto_1fr_auto_1fr] items-center gap-0.5 text-xs leading-none md:grid-cols-[auto_1fr]"
|
||||
>
|
||||
<PhArrowUp class="text-primary" />
|
||||
<span class="tabular-nums">{{ formatBytes(networkRate.tx, { short: true, decimals: 1 }) }}/s</span>
|
||||
<PhArrowDown class="text-secondary" />
|
||||
<span class="tabular-nums">{{ formatBytes(networkRate.rx, { short: true, decimals: 1 }) }}/s</span>
|
||||
</div>
|
||||
<StatMonitor
|
||||
:data="cpuData"
|
||||
:icon="PhCpu"
|
||||
:stat-value="Math.max(0, totalStat.cpu).toFixed(2) + '%'"
|
||||
:limit="roundCPU(limits.cpu) + ' CPU'"
|
||||
container-class="border-primary/40 bg-primary/20"
|
||||
text-class="hover:text-primary"
|
||||
bar-class="bg-primary"
|
||||
:formatter="(value: number) => value.toFixed(2) + '%'"
|
||||
/>
|
||||
<StatMonitor
|
||||
:data="memoryData"
|
||||
:icon="PhMemory"
|
||||
:stat-value="formatBytes(totalStat.memoryUsage)"
|
||||
:limit="formatBytes(limits.memory, { short: true, decimals: 1 })"
|
||||
container-class="border-secondary/40 bg-secondary/20"
|
||||
text-class="hover:text-secondary"
|
||||
bar-class="bg-secondary"
|
||||
:formatter="(value: number) => formatBytes(value)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Stat } from "@/models/Container";
|
||||
import { Container } from "@/models/Container";
|
||||
// @ts-ignore
|
||||
import PhCpu from "~icons/ph/cpu";
|
||||
// @ts-ignore
|
||||
import PhMemory from "~icons/ph/memory";
|
||||
|
||||
const { containers } = defineProps<{
|
||||
containers: Container[];
|
||||
}>();
|
||||
|
||||
const totalStat = ref<Stat>({ cpu: 0, memory: 0, memoryUsage: 0 });
|
||||
const totalStat = ref<Stat>({ cpu: 0, memory: 0, memoryUsage: 0, networkRxTotal: 0, networkTxTotal: 0 });
|
||||
const { history, reset } = useSimpleRefHistory(totalStat, { capacity: 300 });
|
||||
const { hosts } = useHosts();
|
||||
const networkRate = ref({ rx: 0, tx: 0 });
|
||||
|
||||
const roundCPU = (num: number) => (Number.isInteger(num) ? num.toFixed(0) : num.toFixed(1));
|
||||
|
||||
function toContainerCores(container: Container): number {
|
||||
if (container.cpuLimit && container.cpuLimit > 0) {
|
||||
return 1;
|
||||
}
|
||||
const hostInfo = hosts.value[container.host];
|
||||
return hostInfo?.nCPU ?? 1;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => containers,
|
||||
@@ -22,52 +64,115 @@ watch(
|
||||
const initial: Stat[] = [];
|
||||
for (let i = 1; i <= 300; i++) {
|
||||
const stat = containers.reduce(
|
||||
(acc, { statsHistory }) => {
|
||||
const item = statsHistory.at(-i);
|
||||
(acc, container) => {
|
||||
const item = container.statsHistory.at(-i);
|
||||
if (!item) {
|
||||
return acc;
|
||||
}
|
||||
const cores = toContainerCores(container);
|
||||
return {
|
||||
cpu: acc.cpu + item.cpu,
|
||||
cpu: acc.cpu + item.cpu / cores,
|
||||
memory: acc.memory + item.memory,
|
||||
memoryUsage: acc.memoryUsage + item.memoryUsage,
|
||||
networkRxTotal: acc.networkRxTotal + item.networkRxTotal,
|
||||
networkTxTotal: acc.networkTxTotal + item.networkTxTotal,
|
||||
};
|
||||
},
|
||||
{ cpu: 0, memory: 0, memoryUsage: 0 },
|
||||
{ cpu: 0, memory: 0, memoryUsage: 0, networkRxTotal: 0, networkTxTotal: 0 },
|
||||
);
|
||||
initial.push(stat);
|
||||
}
|
||||
totalStat.value = initial[0];
|
||||
reset({ initial: initial.reverse() });
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const limits = computed(() => {
|
||||
// Group containers by host
|
||||
const containersByHost = new Map<string, Container[]>();
|
||||
containers.forEach((container) => {
|
||||
if (!containersByHost.has(container.host)) {
|
||||
containersByHost.set(container.host, []);
|
||||
}
|
||||
containersByHost.get(container.host)!.push(container);
|
||||
});
|
||||
|
||||
let totalCpu = 0;
|
||||
let totalMemory = 0;
|
||||
|
||||
// Process each host independently
|
||||
containersByHost.forEach((hostContainers, hostId) => {
|
||||
const hostInfo = hosts.value[hostId];
|
||||
const hostTotalMemory = hostInfo?.memTotal || 0;
|
||||
const hostTotalCpu = hostInfo?.nCPU || 0;
|
||||
|
||||
// Check if any container lacks limits
|
||||
const hasUnlimitedCpu = hostContainers.some((c) => !c.cpuLimit || c.cpuLimit <= 0);
|
||||
const hasUnlimitedMemory = hostContainers.some((c) => !c.memoryLimit);
|
||||
|
||||
// Calculate CPU for this host
|
||||
if (hasUnlimitedCpu) {
|
||||
// At least one container has no limit, use host total
|
||||
totalCpu += hostTotalCpu;
|
||||
} else {
|
||||
// All containers have limits, sum them up (capped at host total)
|
||||
const sumCpu = hostContainers.reduce((sum, c) => sum + (c.cpuLimit || 0), 0);
|
||||
totalCpu += Math.min(sumCpu, hostTotalCpu);
|
||||
}
|
||||
|
||||
// Calculate Memory for this host
|
||||
if (hasUnlimitedMemory) {
|
||||
// At least one container has no limit, use host total
|
||||
totalMemory += hostTotalMemory;
|
||||
} else {
|
||||
// All containers have limits, sum them up (capped at host total)
|
||||
const sumMemory = hostContainers.reduce((sum, c) => sum + (c.memoryLimit || 0), 0);
|
||||
totalMemory += Math.min(sumMemory, hostTotalMemory);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
cpu: totalCpu,
|
||||
memory: totalMemory,
|
||||
};
|
||||
});
|
||||
|
||||
useIntervalFn(() => {
|
||||
const previousStat = totalStat.value;
|
||||
totalStat.value = containers.reduce(
|
||||
(acc, { stat }) => {
|
||||
(acc, container) => {
|
||||
const cores = toContainerCores(container);
|
||||
return {
|
||||
cpu: acc.cpu + stat.cpu,
|
||||
memory: acc.memory + stat.memory,
|
||||
memoryUsage: acc.memoryUsage + stat.memoryUsage,
|
||||
cpu: acc.cpu + container.stat.cpu / cores,
|
||||
memory: acc.memory + container.stat.memory,
|
||||
memoryUsage: acc.memoryUsage + container.stat.memoryUsage,
|
||||
networkRxTotal: acc.networkRxTotal + container.stat.networkRxTotal,
|
||||
networkTxTotal: acc.networkTxTotal + container.stat.networkTxTotal,
|
||||
};
|
||||
},
|
||||
{ cpu: 0, memory: 0, memoryUsage: 0 },
|
||||
{ cpu: 0, memory: 0, memoryUsage: 0, networkRxTotal: 0, networkTxTotal: 0 },
|
||||
);
|
||||
|
||||
networkRate.value = {
|
||||
rx: Math.max(0, totalStat.value.networkRxTotal - previousStat.networkRxTotal),
|
||||
tx: Math.max(0, totalStat.value.networkTxTotal - previousStat.networkTxTotal),
|
||||
};
|
||||
}, 1000);
|
||||
|
||||
const cpuData = computed(() =>
|
||||
history.value.map((stat, i) => ({
|
||||
x: i,
|
||||
y: Math.max(0, stat.cpu),
|
||||
value: Math.max(0, stat.cpu).toFixed(2) + "%",
|
||||
value: Math.max(0, stat.cpu),
|
||||
})),
|
||||
);
|
||||
|
||||
const memoryData = computed(() =>
|
||||
history.value.map((stat, i) => ({
|
||||
x: i,
|
||||
y: stat.memoryUsage,
|
||||
value: formatBytes(stat.memoryUsage),
|
||||
y: stat.memory,
|
||||
value: stat.memoryUsage,
|
||||
})),
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="tag grid overflow-hidden rounded-sm text-center text-sm text-white">
|
||||
<div class="random-color col-start-1 row-start-1 brightness-75"></div>
|
||||
<div class="col-start-1 row-start-1 truncate px-2 brightness-100" :class="truncateRight ? '[direction:rtl]' : ''">
|
||||
{{ value }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
const colors = [
|
||||
"hsl(200, 85%, 65%)", // Vibrant Sky Blue
|
||||
"hsl(150, 85%, 65%)", // Vibrant Mint
|
||||
"hsl(300, 85%, 65%)", // Vibrant Purple
|
||||
"hsl(25, 85%, 65%)", // Vibrant Peach
|
||||
"hsl(270, 85%, 65%)", // Vibrant Lavender
|
||||
"hsl(340, 85%, 65%)", // Vibrant Rose
|
||||
"hsl(170, 85%, 65%)", // Vibrant Aqua
|
||||
"hsl(50, 85%, 65%)", // Vibrant Yellow
|
||||
"hsl(235, 85%, 65%)", // Vibrant Periwinkle
|
||||
"hsl(10, 85%, 65%)", // Vibrant Coral
|
||||
"hsl(180, 85%, 65%)", // Vibrant Turquoise
|
||||
"hsl(320, 85%, 65%)", // Vibrant Orchid
|
||||
"hsl(90, 85%, 65%)", // Vibrant Lime
|
||||
"hsl(260, 85%, 65%)", // Vibrant Amethyst
|
||||
"hsl(30, 85%, 65%)", // Vibrant Orange
|
||||
"hsl(210, 85%, 65%)", // Vibrant Ocean
|
||||
"hsl(290, 85%, 65%)", // Vibrant Mauve
|
||||
"hsl(120, 85%, 65%)", // Vibrant Spring
|
||||
"hsl(350, 85%, 65%)", // Vibrant Strawberry
|
||||
"hsl(190, 85%, 65%)", // Vibrant Azure
|
||||
] as const;
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
const { value, truncateRight = false } = defineProps<{
|
||||
value: string;
|
||||
truncateRight?: boolean;
|
||||
}>();
|
||||
|
||||
const color = computed(() => colors[Math.abs(hashCode(value)) % colors.length]);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.random-color {
|
||||
background-color: v-bind(color);
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<table class="table table-zebra table-pin-rows table-md" v-if="!loading">
|
||||
<table class="table-zebra table-pin-rows table-md table" v-if="!loading">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="column in columns" :key="column">{{ column }}</th>
|
||||
@@ -11,18 +11,18 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="table table-md animate-pulse" v-else>
|
||||
<table class="table-md table animate-pulse" v-else>
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="_ in 3">
|
||||
<div class="h-4 w-20 animate-pulse bg-base-content/50 opacity-50"></div>
|
||||
<div class="bg-base-content/50 h-4 w-20 animate-pulse opacity-50"></div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="_ in 9">
|
||||
<td v-for="_ in 3">
|
||||
<div class="h-4 w-20 bg-base-content/50 opacity-20"></div>
|
||||
<div class="bg-base-content/50 h-4 w-20 opacity-20"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -1,44 +1,25 @@
|
||||
<template>
|
||||
<div class="relative flex w-full items-start gap-x-2">
|
||||
<LogStd :std="logEntry.std" v-if="showStd" />
|
||||
<ContainerName class="flex-none" :id="logEntry.containerID" v-if="showContainerName" />
|
||||
<LogDate :date="logEntry.date" v-if="showTimestamp" class="select-none" />
|
||||
<LogLevel class="flex" :level="logEntry.level" :position="logEntry.position" />
|
||||
<LogItem :logEntry>
|
||||
<LogLevel class="flex select-none" :level="logEntry.level" />
|
||||
<div
|
||||
class="log-wrapper whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap"
|
||||
v-html="linkify(colorize(logEntry.message))"
|
||||
class="[word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-pre"
|
||||
v-html="colorize(logEntry.message)"
|
||||
></div>
|
||||
<LogMessageActions
|
||||
class="duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100"
|
||||
:message="() => decodeXML(stripAnsi(logEntry.message))"
|
||||
:log-entry="logEntry"
|
||||
/>
|
||||
</div>
|
||||
</LogItem>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { SimpleLogEntry } from "@/models/LogEntry";
|
||||
import { decodeXML } from "entities";
|
||||
import AnsiConvertor from "ansi-to-html";
|
||||
import stripAnsi from "strip-ansi";
|
||||
|
||||
const ansiConvertor = new AnsiConvertor({
|
||||
escapeXML: false,
|
||||
fg: "var(--base-content-color)",
|
||||
bg: "var(--base-color)",
|
||||
fg: "var(--color-base-content)",
|
||||
bg: "var(--color-base-100)",
|
||||
});
|
||||
|
||||
const { showContainerName = false } = defineProps<{
|
||||
defineProps<{
|
||||
logEntry: SimpleLogEntry;
|
||||
showContainerName?: boolean;
|
||||
}>();
|
||||
|
||||
const colorize = (value: string) => ansiConvertor.toHtml(value);
|
||||
const urlPattern = /(https?:\/\/[^\s]+)/g;
|
||||
const linkify = (text: string) =>
|
||||
text.replace(urlPattern, (url) => `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`);
|
||||
</script>
|
||||
<style scoped lang="postcss">
|
||||
.log-wrapper > :deep(a) {
|
||||
@apply text-primary underline-offset-4 hover:underline;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
<div class="my-4 flex-1 text-center">
|
||||
<div class="relative">
|
||||
<ZigZag class="absolute inset-0 mt-2" />
|
||||
<span class="relative whitespace-pre-wrap bg-base px-4 py-2 font-bold">
|
||||
<button class="btn btn-primary btn-xs relative whitespace-pre-wrap" @click="logEntry.loadSkippedEntries()">
|
||||
{{ $t("error.logs-skipped", { total: logEntry.totalSkipped }) }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { SkippedLogsEntry } from "@/models/LogEntry";
|
||||
|
||||
defineProps<{
|
||||
const { logEntry } = defineProps<{
|
||||
logEntry: SkippedLogsEntry;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
@@ -1,25 +1,66 @@
|
||||
<template>
|
||||
<div class="relative hover:text-secondary" @mouseenter="mouseOver = true" @mouseleave="mouseOver = false">
|
||||
<div class="hidden overflow-hidden rounded-sm border border-primary px-px pb-px pt-1 md:flex">
|
||||
<StatSparkline :data="data" @selected-point="onSelectedPoint" />
|
||||
<div class="relative" @mouseenter="mouseOver = true" @mouseleave="mouseOver = false" :class="textClass">
|
||||
<div class="overflow-hidden rounded-xs border px-px pt-1 pb-px max-md:hidden" :class="containerClass">
|
||||
<BarChart
|
||||
:chart-data="chartData"
|
||||
:bar-class="`${barClass} opacity-70 hover:opacity-100`"
|
||||
class="h-8 w-44"
|
||||
@hover-index="(startIndex: number, endIndex: number) => onHoverIndexChange(startIndex, endIndex)"
|
||||
/>
|
||||
</div>
|
||||
<div class="inline-flex gap-1 rounded bg-base p-px text-xs md:absolute md:-left-0.5 md:-top-2">
|
||||
<div class="font-light uppercase">{{ label }}</div>
|
||||
<div class="select-none font-bold">
|
||||
{{ mouseOver ? selectedPoint?.value ?? selectedPoint?.y ?? statValue : statValue }}
|
||||
<div class="bg-base-200 flex gap-1 rounded-sm p-px text-xs md:absolute md:-top-2 md:-left-0.5">
|
||||
<component :is="icon" class="text-sm" />
|
||||
<div class="font-bold tabular-nums select-none">
|
||||
{{ displayValue }}
|
||||
<span v-if="limit !== -1 && !mouseOver" class="max-md:hidden"> / {{ limit }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const { data, label, statValue } = defineProps<{ data: Point<unknown>[]; label: string; statValue: string | number }>();
|
||||
import type { Component } from "vue";
|
||||
|
||||
let selectedPoint: Point<unknown> | undefined = $ref();
|
||||
const {
|
||||
data,
|
||||
icon,
|
||||
statValue,
|
||||
limit = -1,
|
||||
containerClass = "border-primary",
|
||||
textClass = "",
|
||||
barClass = "bg-primary",
|
||||
formatter,
|
||||
} = defineProps<{
|
||||
data: Point<unknown>[];
|
||||
icon: Component;
|
||||
statValue: string | number;
|
||||
limit?: string | number;
|
||||
containerClass?: string;
|
||||
textClass?: string;
|
||||
barClass?: string;
|
||||
formatter?: (value: number) => string;
|
||||
}>();
|
||||
|
||||
function onSelectedPoint(point: Point<unknown>) {
|
||||
selectedPoint = point;
|
||||
const chartData = computed(() => data.map((point) => (point.y as number) ?? 0));
|
||||
const mouseOver = ref(false);
|
||||
const hoveredRange = ref<{ start: number; end: number } | null>(null);
|
||||
|
||||
function onHoverIndexChange(startIndex: number, endIndex: number) {
|
||||
hoveredRange.value = { start: startIndex, end: endIndex };
|
||||
}
|
||||
|
||||
let mouseOver = $ref(false);
|
||||
const displayValue = computed(() => {
|
||||
if (mouseOver.value && hoveredRange.value !== null) {
|
||||
const { start, end } = hoveredRange.value;
|
||||
const points = data.slice(start, end + 1);
|
||||
const sum = points.reduce((acc, point) => acc + ((point.value as number) ?? (point.y as number) ?? 0), 0);
|
||||
const avg = sum / points.length;
|
||||
|
||||
if (formatter) {
|
||||
return formatter(avg);
|
||||
}
|
||||
return avg.toFixed(2);
|
||||
}
|
||||
return statValue;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<template>
|
||||
<svg :width="width" :height="height" @mousemove="onMove" class="group">
|
||||
<path :d="path" class="fill-primary" />
|
||||
<line :x1="lineX" y1="0" :x2="lineX" :y2="height" class="invisible stroke-secondary stroke-2 group-hover:visible" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { extent } from "d3-array";
|
||||
import { scaleLinear } from "d3-scale";
|
||||
import { area, curveStep } from "d3-shape";
|
||||
|
||||
const d3 = { extent, scaleLinear, area, curveStep };
|
||||
const { data, width = 150, height = 30 } = defineProps<{ data: Point<unknown>[]; width?: number; height?: number }>();
|
||||
const x = d3.scaleLinear().range([0, width]);
|
||||
const y = d3.scaleLinear().range([height, 0]);
|
||||
|
||||
const selectedPoint = defineEmit<[value: Point<unknown>]>();
|
||||
|
||||
const shape = d3
|
||||
.area<Point<unknown>>()
|
||||
.curve(d3.curveStep)
|
||||
.x((d) => x(d.x))
|
||||
.y0(height)
|
||||
.y1((d) => y(d.y));
|
||||
|
||||
const path = computed(() => {
|
||||
x.domain(d3.extent(data, (d) => d.x) as [number, number]);
|
||||
y.domain(d3.extent([...data, { y: 1 }], (d) => d.y) as [number, number]);
|
||||
|
||||
return shape(data) ?? "";
|
||||
});
|
||||
|
||||
let lineX = $ref(0);
|
||||
|
||||
function onMove(e: MouseEvent) {
|
||||
const { offsetX } = e;
|
||||
const xValue = x.invert(offsetX);
|
||||
const index = Math.round(xValue);
|
||||
lineX = x(index);
|
||||
const point = data[index];
|
||||
selectedPoint(point);
|
||||
}
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<EventSource ref="source" #default="{ messages }" :stream-source="streamSource" :entity="entity">
|
||||
<LogViewer :messages="messages" :visible-keys="visibleKeys" :show-container-name="showContainerName" />
|
||||
<LogViewer :messages="messages" :visible-keys="visibleKeys" />
|
||||
</EventSource>
|
||||
</template>
|
||||
|
||||
@@ -9,10 +9,9 @@ import EventSource from "@/components/LogViewer/EventSource.vue";
|
||||
import { LogStreamSource } from "@/composable/eventStreams";
|
||||
import { ComponentExposed } from "vue-component-type-helpers";
|
||||
|
||||
const { streamSource, visibleKeys, showContainerName, entity } = defineProps<{
|
||||
const { streamSource, visibleKeys, entity } = defineProps<{
|
||||
streamSource: (t: Ref<T>) => LogStreamSource;
|
||||
visibleKeys: Map<string[], boolean>;
|
||||
showContainerName: boolean;
|
||||
entity: T;
|
||||
}>();
|
||||
|
||||
|
||||
@@ -1,75 +1,129 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<ContainerEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
|
||||
"<ul data-v-cf9ff940="" class="events group pt-4 medium">
|
||||
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
|
||||
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2">
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<div data-v-961504e7="" data-v-a49e52d4="" class="tag inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] select-none" size="small">
|
||||
<div class="inline-flex gap-2 whitespace-nowrap text-blue"><time datetime="2019-06-12T10:55:42.459Z" class="mobile-hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42 AM</time></div>
|
||||
"<ul data-v-cf9ff940="" class="group pt-4 medium" data-logs="" show-container-name="false">
|
||||
<li data-v-cf9ff940="" id="1560336942709" data-time="1560336942709" class="group/entry">
|
||||
<div data-v-cf9ff940="" class="flex min-h-[1px] flex-1 content-center justify-center"><span class="loading loading-bars loading-md text-primary m-2" style="display: none;"></span></div>
|
||||
</li>
|
||||
<li data-v-cf9ff940="" id="1" data-time="1560336942459" class="group/entry">
|
||||
<div data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
|
||||
<div class="dropdown dropdown-hover absolute -left-2 z-10 font-sans dropdown-right dropdown-end"><button tabindex="0" class="btn btn-square btn-xs border-base-content/20 bg-base-100 border opacity-0 shadow-sm group-hover/entry:opacity-90"><svg viewBox="0 0 512 512" width="1.2em" height="1.2em">
|
||||
<circle cx="256" cy="256" r="48" fill="currentColor"></circle>
|
||||
<circle cx="256" cy="416" r="48" fill="currentColor"></circle>
|
||||
<circle cx="256" cy="96" r="48" fill="currentColor"></circle>
|
||||
</svg></button>
|
||||
<ul tabindex="0" class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 text-sm shadow-sm">
|
||||
<!--v-if-->
|
||||
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
|
||||
<path fill="currentColor" d="M9 18q-.825 0-1.412-.587T7 16V4q0-.825.588-1.412T9 2h9q.825 0 1.413.588T20 4v12q0 .825-.587 1.413T18 18zm-4 4q-.825 0-1.412-.587T3 20V6h2v14h11v2z"></path>
|
||||
</svg> action.copy-log</a></li>
|
||||
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
|
||||
<path fill="currentColor" d="M11 17H7q-2.075 0-3.537-1.463T2 12t1.463-3.537T7 7h4v2H7q-1.25 0-2.125.875T4 12t.875 2.125T7 15h4zm-3-4v-2h8v2zm5 4v-2h4q1.25 0 2.125-.875T20 12t-.875-2.125T17 9h-4V7h4q2.075 0 3.538 1.463T22 12t-1.463 3.538T17 17z"></path>
|
||||
</svg> action.copy-link</a></li>
|
||||
<!--v-if-->
|
||||
</ul>
|
||||
</div>
|
||||
<div data-v-e625cddd="" data-v-a49e52d4="" class="mt-1.5 size-2.5 flex-none rounded-lg flex"></div>
|
||||
<div data-v-a49e52d4="" class="log-wrapper whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap">foo bar</div>
|
||||
<div data-v-a49e52d4="" class="flex gap-2 duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100">
|
||||
<!--v-if-->
|
||||
<div class="flex gap-x-2 gap-y-1 group-[.compact]:gap-y-0 has-[>_*:nth-of-type(2)]:flex-col-reverse md:flex-row!">
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<div class="tag bg-base-100 inline-flex items-center justify-center rounded-sm px-2 py-[0.2em] [[size='small']]:text-[0.8rem] items-start! shrink-0 select-none">
|
||||
<div class="inline-flex gap-2 text-blue whitespace-nowrap"><time datetime="2019-06-12T10:55:42.459Z" class="max-md:hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42 AM</time></div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-v-e625cddd="" class="mt-1.5 size-2.5 flex-none rounded-lg flex select-none"></div>
|
||||
<div class="[word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-pre">foo bar</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>"
|
||||
`;
|
||||
|
||||
exports[`<ContainerEventSource /> > render html correctly > should render dates with 24 hour style 1`] = `
|
||||
"<ul data-v-cf9ff940="" class="events group pt-4 medium">
|
||||
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
|
||||
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2">
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<div data-v-961504e7="" data-v-a49e52d4="" class="tag inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] select-none" size="small">
|
||||
<div class="inline-flex gap-2 whitespace-nowrap text-blue"><time datetime="2019-06-12T10:55:42.459Z" class="mobile-hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42</time></div>
|
||||
"<ul data-v-cf9ff940="" class="group pt-4 medium" data-logs="" show-container-name="false">
|
||||
<li data-v-cf9ff940="" id="1560336942709" data-time="1560336942709" class="group/entry">
|
||||
<div data-v-cf9ff940="" class="flex min-h-[1px] flex-1 content-center justify-center"><span class="loading loading-bars loading-md text-primary m-2" style="display: none;"></span></div>
|
||||
</li>
|
||||
<li data-v-cf9ff940="" id="1" data-time="1560336942459" class="group/entry">
|
||||
<div data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
|
||||
<div class="dropdown dropdown-hover absolute -left-2 z-10 font-sans dropdown-right dropdown-end"><button tabindex="0" class="btn btn-square btn-xs border-base-content/20 bg-base-100 border opacity-0 shadow-sm group-hover/entry:opacity-90"><svg viewBox="0 0 512 512" width="1.2em" height="1.2em">
|
||||
<circle cx="256" cy="256" r="48" fill="currentColor"></circle>
|
||||
<circle cx="256" cy="416" r="48" fill="currentColor"></circle>
|
||||
<circle cx="256" cy="96" r="48" fill="currentColor"></circle>
|
||||
</svg></button>
|
||||
<ul tabindex="0" class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 text-sm shadow-sm">
|
||||
<!--v-if-->
|
||||
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
|
||||
<path fill="currentColor" d="M9 18q-.825 0-1.412-.587T7 16V4q0-.825.588-1.412T9 2h9q.825 0 1.413.588T20 4v12q0 .825-.587 1.413T18 18zm-4 4q-.825 0-1.412-.587T3 20V6h2v14h11v2z"></path>
|
||||
</svg> action.copy-log</a></li>
|
||||
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
|
||||
<path fill="currentColor" d="M11 17H7q-2.075 0-3.537-1.463T2 12t1.463-3.537T7 7h4v2H7q-1.25 0-2.125.875T4 12t.875 2.125T7 15h4zm-3-4v-2h8v2zm5 4v-2h4q1.25 0 2.125-.875T20 12t-.875-2.125T17 9h-4V7h4q2.075 0 3.538 1.463T22 12t-1.463 3.538T17 17z"></path>
|
||||
</svg> action.copy-link</a></li>
|
||||
<!--v-if-->
|
||||
</ul>
|
||||
</div>
|
||||
<div data-v-e625cddd="" data-v-a49e52d4="" class="mt-1.5 size-2.5 flex-none rounded-lg flex"></div>
|
||||
<div data-v-a49e52d4="" class="log-wrapper whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap">foo bar</div>
|
||||
<div data-v-a49e52d4="" class="flex gap-2 duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100">
|
||||
<!--v-if-->
|
||||
<div class="flex gap-x-2 gap-y-1 group-[.compact]:gap-y-0 has-[>_*:nth-of-type(2)]:flex-col-reverse md:flex-row!">
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<div class="tag bg-base-100 inline-flex items-center justify-center rounded-sm px-2 py-[0.2em] [[size='small']]:text-[0.8rem] items-start! shrink-0 select-none">
|
||||
<div class="inline-flex gap-2 text-blue whitespace-nowrap"><time datetime="2019-06-12T10:55:42.459Z" class="max-md:hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42</time></div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-v-e625cddd="" class="mt-1.5 size-2.5 flex-none rounded-lg flex select-none"></div>
|
||||
<div class="[word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-pre">foo bar</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>"
|
||||
`;
|
||||
|
||||
exports[`<ContainerEventSource /> > render html correctly > should render messages 1`] = `
|
||||
"<ul data-v-cf9ff940="" class="events group pt-4 medium">
|
||||
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
|
||||
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2">
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<div data-v-961504e7="" data-v-a49e52d4="" class="tag inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] select-none" size="small">
|
||||
<div class="inline-flex gap-2 whitespace-nowrap text-blue"><time datetime="2019-06-12T10:55:42.459Z" class="mobile-hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42 AM</time></div>
|
||||
"<ul data-v-cf9ff940="" class="group pt-4 medium" data-logs="" show-container-name="false">
|
||||
<li data-v-cf9ff940="" id="1560336942709" data-time="1560336942709" class="group/entry">
|
||||
<div data-v-cf9ff940="" class="flex min-h-[1px] flex-1 content-center justify-center"><span class="loading loading-bars loading-md text-primary m-2" style="display: none;"></span></div>
|
||||
</li>
|
||||
<li data-v-cf9ff940="" id="1" data-time="1560336942459" class="group/entry">
|
||||
<div data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
|
||||
<div class="dropdown dropdown-hover absolute -left-2 z-10 font-sans dropdown-right dropdown-end"><button tabindex="0" class="btn btn-square btn-xs border-base-content/20 bg-base-100 border opacity-0 shadow-sm group-hover/entry:opacity-90"><svg viewBox="0 0 512 512" width="1.2em" height="1.2em">
|
||||
<circle cx="256" cy="256" r="48" fill="currentColor"></circle>
|
||||
<circle cx="256" cy="416" r="48" fill="currentColor"></circle>
|
||||
<circle cx="256" cy="96" r="48" fill="currentColor"></circle>
|
||||
</svg></button>
|
||||
<ul tabindex="0" class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 text-sm shadow-sm">
|
||||
<!--v-if-->
|
||||
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
|
||||
<path fill="currentColor" d="M9 18q-.825 0-1.412-.587T7 16V4q0-.825.588-1.412T9 2h9q.825 0 1.413.588T20 4v12q0 .825-.587 1.413T18 18zm-4 4q-.825 0-1.412-.587T3 20V6h2v14h11v2z"></path>
|
||||
</svg> action.copy-log</a></li>
|
||||
<li><a disabled="true" title="error.copy-not-supported" class="cursor-not-allowed opacity-50"><svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
|
||||
<path fill="currentColor" d="M11 17H7q-2.075 0-3.537-1.463T2 12t1.463-3.537T7 7h4v2H7q-1.25 0-2.125.875T4 12t.875 2.125T7 15h4zm-3-4v-2h8v2zm5 4v-2h4q1.25 0 2.125-.875T20 12t-.875-2.125T17 9h-4V7h4q2.075 0 3.538 1.463T22 12t-1.463 3.538T17 17z"></path>
|
||||
</svg> action.copy-link</a></li>
|
||||
<!--v-if-->
|
||||
</ul>
|
||||
</div>
|
||||
<div data-v-e625cddd="" data-v-a49e52d4="" class="mt-1.5 size-2.5 flex-none rounded-lg flex"></div>
|
||||
<div data-v-a49e52d4="" class="log-wrapper whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap">This is a message.</div>
|
||||
<div data-v-a49e52d4="" class="flex gap-2 duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100">
|
||||
<!--v-if-->
|
||||
<div class="flex gap-x-2 gap-y-1 group-[.compact]:gap-y-0 has-[>_*:nth-of-type(2)]:flex-col-reverse md:flex-row!">
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<div class="tag bg-base-100 inline-flex items-center justify-center rounded-sm px-2 py-[0.2em] [[size='small']]:text-[0.8rem] items-start! shrink-0 select-none">
|
||||
<div class="inline-flex gap-2 text-blue whitespace-nowrap"><time datetime="2019-06-12T10:55:42.459Z" class="max-md:hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42 AM</time></div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-v-e625cddd="" class="mt-1.5 size-2.5 flex-none rounded-lg flex select-none"></div>
|
||||
<div class="[word-break:break-word] whitespace-pre-wrap group-[.disable-wrap]:whitespace-pre">This is a message.</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>"
|
||||
`;
|
||||
|
||||
exports[`<ContainerEventSource /> > renders loading correctly 1`] = `
|
||||
"<div class="flex min-h-[1px] justify-center"><span class="loading loading-bars loading-md mt-4 text-primary" style="display: none;"></span></div>
|
||||
<!--v-if-->"
|
||||
`;
|
||||
|
||||
exports[`<ContainerEventSource /> > should parse messages 1`] = `
|
||||
SimpleLogEntry {
|
||||
"_message": "This is a message.",
|
||||
"containerID": undefined,
|
||||
"date": 2019-06-12T10:55:42.459Z,
|
||||
"id": 1,
|
||||
LoadMoreLogEntry {
|
||||
"_message": "",
|
||||
"containerID": "",
|
||||
"date": 2019-06-12T10:55:42.709Z,
|
||||
"id": 1560336942709,
|
||||
"level": undefined,
|
||||
"position": undefined,
|
||||
"loader": [Function],
|
||||
"rawMessage": "info",
|
||||
"rememberScrollPosition": true,
|
||||
"std": "stderr",
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="rounded-lg border p-2 md:p-3" :class="containerClass">
|
||||
<div class="mb-2 flex items-center gap-1.5 text-sm font-medium" :class="textClass">
|
||||
<component :is="icon" class="text-lg" />
|
||||
<span>{{ label }}</span>
|
||||
</div>
|
||||
<div class="mb-1.5 text-lg font-semibold tabular-nums">{{ formattedValue }}</div>
|
||||
<div class="text-base-content/60 mb-1 text-xs tabular-nums max-md:hidden">
|
||||
avg {{ formatValue(average) }} • pk {{ formatValue(peak) }}
|
||||
</div>
|
||||
<BarChart class="h-8" :chartData="percentData" :barClass="barClass" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Component } from "vue";
|
||||
|
||||
export interface MetricDataPoint {
|
||||
percent: number; // value 0 - 100
|
||||
value: number;
|
||||
}
|
||||
|
||||
const {
|
||||
label,
|
||||
icon,
|
||||
value,
|
||||
chartData,
|
||||
containerClass = "",
|
||||
textClass = "",
|
||||
barClass = "",
|
||||
formatValue = (v: number) => v.toString(),
|
||||
} = defineProps<{
|
||||
label: string;
|
||||
icon: Component;
|
||||
value: string | number;
|
||||
chartData: MetricDataPoint[];
|
||||
containerClass?: string;
|
||||
textClass?: string;
|
||||
barClass?: string;
|
||||
formatValue?: (value: number) => string;
|
||||
}>();
|
||||
|
||||
const percentData = computed(() => chartData.map((d) => d.percent));
|
||||
|
||||
const peak = computed(() => (chartData.length > 0 ? Math.max(...chartData.map((d) => d.value)) : 0));
|
||||
|
||||
const average = computed(() => {
|
||||
if (chartData.length === 0) return 0;
|
||||
return chartData.reduce((sum, d) => sum + d.value, 0) / chartData.length;
|
||||
});
|
||||
|
||||
const formattedValue = computed(() => {
|
||||
if (typeof value === "string") return value;
|
||||
return formatValue(value);
|
||||
});
|
||||
</script>
|
||||
@@ -1,14 +1,11 @@
|
||||
<template>
|
||||
<ScrollableView :scrollable="scrollable" v-if="containers.length && ready">
|
||||
<template #header>
|
||||
<div class="mx-2 flex items-center gap-2 md:ml-4">
|
||||
<div class="flex flex-1 gap-1.5 truncate @container md:gap-2">
|
||||
<div class="inline-flex font-mono text-sm">
|
||||
<div class="font-semibold">{{ containers.length }} containers</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-2 flex items-center gap-1 md:ml-4 md:gap-2">
|
||||
<octicon:container-24 />
|
||||
<ContainerDropdown :containers="containers">{{ $t("label.container", containers.length) }}</ContainerDropdown>
|
||||
<MultiContainerStat class="ml-auto" :containers="containers" />
|
||||
<MultiContainerActionToolbar class="mobile-hidden" @clear="viewer?.clear()" />
|
||||
<MultiContainerActionToolbar @clear="viewer?.clear()" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
@@ -17,7 +14,6 @@
|
||||
:stream-source="useMergedStream"
|
||||
:entity="containers"
|
||||
:visible-keys="new Map<string[], boolean>()"
|
||||
:show-container-name="true"
|
||||
/>
|
||||
</template>
|
||||
</ScrollableView>
|
||||
@@ -37,5 +33,5 @@ const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
|
||||
const { allContainersById, ready } = storeToRefs(containerStore);
|
||||
const containers = computed(() => ids.map((id) => allContainersById.value[id]));
|
||||
|
||||
provideLoggingContext(containers);
|
||||
provideLoggingContext(containers, { showContainerName: true, showHostname: false });
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-8 px-4 py-4 md:px-8">
|
||||
<div class="flex flex-col gap-5 px-4 py-4 md:px-8">
|
||||
<section>
|
||||
<Links>
|
||||
<template #more-items>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<teleport to="body">
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-show="show && (delayedShow || glopbalShow)"
|
||||
class="fixed z-50 rounded border border-base-content/20 bg-base-lighter p-4 shadow"
|
||||
v-show="show && (delayedShow || globalShow)"
|
||||
class="ring-base-content/20 bg-base-100 fixed z-50 rounded-sm p-3 shadow-sm ring"
|
||||
ref="content"
|
||||
>
|
||||
<slot name="content"></slot>
|
||||
@@ -16,14 +16,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { globalShowPopup } from "@/composable/popup";
|
||||
|
||||
const glopbalShow = globalShowPopup();
|
||||
const show = ref(glopbalShow.value);
|
||||
const globalShow = globalShowPopup();
|
||||
const show = ref(globalShow.value);
|
||||
const delayedShow = refDebounced(show, 1000);
|
||||
const content = ref<HTMLElement>();
|
||||
|
||||
const onMouseEnter = (e: Event) => {
|
||||
show.value = true;
|
||||
glopbalShow.value = true;
|
||||
globalShow.value = true;
|
||||
|
||||
if (content.value && e.target instanceof HTMLElement) {
|
||||
const { left, top, width } = e.target.getBoundingClientRect();
|
||||
@@ -37,7 +37,7 @@ const onMouseEnter = (e: Event) => {
|
||||
|
||||
const onMouseLeave = () => {
|
||||
show.value = false;
|
||||
glopbalShow.value = false;
|
||||
globalShow.value = false;
|
||||
};
|
||||
|
||||
const el: Ref<HTMLElement> = useCurrentElement();
|
||||
@@ -45,7 +45,7 @@ useEventListener(() => el.value?.nextElementSibling, "mouseenter", onMouseEnter)
|
||||
useEventListener(() => el.value?.nextElementSibling, "mouseleave", onMouseLeave);
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
@apply transition-opacity;
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
<template>
|
||||
<ul class="space-y-4 p-2">
|
||||
<li v-for="release in releases" v-if="releases?.length">
|
||||
<div class="flex items-baseline gap-1">
|
||||
<carbon:warning class="self-center stroke-orange" v-if="release.breaking > 0" />
|
||||
<a :href="release.htmlUrl" class="link-primary text-lg font-bold" target="_blank" rel="noreferrer noopener">
|
||||
{{ release.name }}
|
||||
</a>
|
||||
<span class="ml-1 text-xs"><distance-time :date="new Date(release.createdAt)" /></span>
|
||||
<Tag class="ml-auto bg-red px-1 py-1 text-xs" v-if="release.tag === latest?.tag">
|
||||
{{ $t("releases.latest") }}
|
||||
</Tag>
|
||||
</div>
|
||||
<div class="text-sm text-base-content/80">
|
||||
{{ summary(release) }}
|
||||
</div>
|
||||
</li>
|
||||
<li v-else>
|
||||
<div class="text-sm text-base-content/80">
|
||||
{{ $t("releases.no_releases") }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { releases, latest } = useReleases();
|
||||
const { t } = useI18n();
|
||||
|
||||
function summary(release: { features: number; bugFixes: number; breaking: number }) {
|
||||
if (release.features > 0 && release.bugFixes > 0 && release.breaking > 0) {
|
||||
return t("releases.three_parts", {
|
||||
first: t("releases.breaking", { count: release.breaking }),
|
||||
second: t("releases.features", { count: release.features }),
|
||||
third: t("releases.bugFixes", { count: release.bugFixes }),
|
||||
});
|
||||
}
|
||||
|
||||
if (release.features > 0 && release.bugFixes > 0) {
|
||||
return t("releases.two_parts", {
|
||||
first: t("releases.features", { count: release.features }),
|
||||
second: t("releases.bugFixes", { count: release.bugFixes }),
|
||||
});
|
||||
}
|
||||
|
||||
if (release.features > 0 && release.breaking > 0) {
|
||||
return t("releases.two_parts", {
|
||||
first: t("releases.features", { count: release.features }),
|
||||
second: t("releases.breaking", { count: release.breaking }),
|
||||
});
|
||||
}
|
||||
|
||||
if (release.bugFixes > 0 && release.breaking > 0) {
|
||||
return t("releases.two_parts", {
|
||||
first: t("releases.bugFixes", { count: release.bugFixes }),
|
||||
second: t("releases.breaking", { count: release.breaking }),
|
||||
});
|
||||
}
|
||||
|
||||
if (release.features > 0) {
|
||||
return t("releases.features", { count: release.features });
|
||||
}
|
||||
|
||||
if (release.bugFixes > 0) {
|
||||
return t("releases.bugFixes", { count: release.bugFixes });
|
||||
}
|
||||
|
||||
if (release.breaking > 0) {
|
||||
return t("releases.breaking", { count: release.breaking });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="inline-flex flex-col items-end gap-2" ref="root" v-show="!autoHide || show">
|
||||
<div class="relative inline-block">
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" :class="{ indeterminate }">
|
||||
<circle r="44" cx="50" cy="50" class="fill-base-darker stroke-primary" />
|
||||
<circle r="44" cx="50" cy="50" class="fill-base-300 stroke-primary" />
|
||||
</svg>
|
||||
<div class="absolute inset-0 flex items-center justify-center font-light">
|
||||
<span class="text-4xl">
|
||||
@@ -12,7 +12,7 @@
|
||||
<span> % </span>
|
||||
</div>
|
||||
</div>
|
||||
<DistanceTime :date="date" class="whitespace-nowrap text-sm" />
|
||||
<RelativeTime :date="date" class="text-sm whitespace-nowrap" />
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
@@ -41,7 +41,7 @@ watch(
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<style scoped lang="postcss">
|
||||
<style scoped>
|
||||
svg {
|
||||
filter: drop-shadow(0px 1px 1px rgba(0, 0, 0, 0.2));
|
||||
margin-top: 5px;
|
||||
@@ -93,7 +93,7 @@ svg {
|
||||
}
|
||||
|
||||
.fadeout-leave-active {
|
||||
@apply transition-opacity;
|
||||
@apply transition-opacity duration-400;
|
||||
}
|
||||
|
||||
.fadeout-leave-to {
|
||||
|
||||
@@ -2,39 +2,33 @@
|
||||
<section :class="{ 'h-screen min-h-0': scrollable }" class="flex flex-col">
|
||||
<header
|
||||
v-if="$slots.header"
|
||||
class="sticky top-[70px] z-[2] border-b border-base-content/10 bg-base py-2 shadow-[1px_1px_2px_0_rgb(0,0,0,0.05)] md:top-0"
|
||||
class="border-base-content/10 bg-base-200 sticky top-[calc(55px+env(safe-area-inset-top))] z-20 border-b py-0.5 shadow-[1px_1px_2px_0_rgb(0,0,0,0.05)] md:top-0 md:py-2"
|
||||
>
|
||||
<slot name="header"></slot>
|
||||
</header>
|
||||
<main :data-scrolling="scrollable ? true : undefined" class="snap-y overflow-auto">
|
||||
<div class="invisible relative md:visible" v-show="scrollContext.paused">
|
||||
<div class="absolute right-44 top-4">
|
||||
<div class="absolute top-4 right-44">
|
||||
<ScrollProgress
|
||||
:indeterminate="loadingMore"
|
||||
:auto-hide="!loadingMore"
|
||||
:progress="scrollContext.progress"
|
||||
:date="scrollContext.currentDate"
|
||||
class="!fixed z-10 min-w-40"
|
||||
class="fixed! z-10 min-w-40"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="scrollableContent">
|
||||
<slot></slot>
|
||||
<div v-if="scrollContext.loading" class="m-4 text-center">
|
||||
<span class="loading loading-ring loading-md text-primary"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="animate-background h-1 w-1/2 bg-gradient-radial from-primary to-transparent to-75%"
|
||||
v-show="!scrollContext.paused && !scrollContext.loading"
|
||||
></div>
|
||||
|
||||
<div ref="scrollObserver" class="h-px"></div>
|
||||
</main>
|
||||
|
||||
<div class="mr-16 text-right">
|
||||
<div class="mr-16 text-right" v-if="!historical">
|
||||
<transition name="fade">
|
||||
<button
|
||||
class="transition-colorsblur-xs dark btn btn-primary fixed bottom-8 rounded p-3 text-primary-content shadow"
|
||||
class="btn btn-primary text-primary-content fixed bottom-8 rounded-sm p-3 shadow-sm transition-colors"
|
||||
:class="hasMore ? 'btn-secondary animate-bounce-fast text-secondary-content' : ''"
|
||||
@click="scrollToBottom()"
|
||||
v-show="scrollContext.paused"
|
||||
@@ -55,35 +49,36 @@ const scrollableContent = ref<HTMLElement>();
|
||||
|
||||
const scrollContext = provideScrollContext();
|
||||
|
||||
const { loadingMore } = useLoggingContext();
|
||||
const { loadingMore, historical } = useLoggingContext();
|
||||
if (!historical.value) {
|
||||
useIntersectionObserver(scrollObserver, ([entry]) => (scrollContext.paused = entry.intersectionRatio == 0), {
|
||||
threshold: [0, 1],
|
||||
rootMargin: "40px 0px",
|
||||
});
|
||||
|
||||
useIntersectionObserver(scrollObserver, ([entry]) => (scrollContext.paused = entry.intersectionRatio == 0), {
|
||||
threshold: [0, 1],
|
||||
rootMargin: "40px 0px",
|
||||
});
|
||||
|
||||
useMutationObserver(
|
||||
scrollableContent,
|
||||
(records) => {
|
||||
if (!scrollContext.paused) {
|
||||
scrollToBottom();
|
||||
} else {
|
||||
const record = records[records.length - 1];
|
||||
const children = (record.target as HTMLElement).children;
|
||||
if (children[children.length - 1] == record.addedNodes[record.addedNodes.length - 1]) {
|
||||
hasMore.value = true;
|
||||
useMutationObserver(
|
||||
scrollableContent,
|
||||
(records) => {
|
||||
if (!scrollContext.paused) {
|
||||
scrollToBottom();
|
||||
} else {
|
||||
const record = records[records.length - 1];
|
||||
const children = (record.target as HTMLElement).children;
|
||||
if (children[children.length - 1] == record.addedNodes[record.addedNodes.length - 1]) {
|
||||
hasMore.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ childList: true, subtree: true },
|
||||
);
|
||||
},
|
||||
{ childList: true, subtree: true },
|
||||
);
|
||||
}
|
||||
|
||||
function scrollToBottom(behavior: "auto" | "smooth" = "auto") {
|
||||
scrollObserver.value?.scrollIntoView({ behavior });
|
||||
hasMore.value = false;
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="postcss">
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
@apply transition-opacity;
|
||||
@@ -93,20 +88,6 @@ function scrollToBottom(behavior: "auto" | "smooth" = "auto") {
|
||||
.fade-leave-to {
|
||||
@apply opacity-0;
|
||||
}
|
||||
|
||||
.animate-background {
|
||||
animation: gradient-animation 3s ease-out infinite;
|
||||
}
|
||||
|
||||
@keyframes gradient-animation {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0%);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
<template>
|
||||
<transition name="slide">
|
||||
<div
|
||||
class="fixed z-10 flex w-full justify-end p-2"
|
||||
class="fixed z-50 flex w-full justify-end p-2"
|
||||
v-show="showSearch"
|
||||
v-if="search"
|
||||
ref="container"
|
||||
:style="style"
|
||||
>
|
||||
<div
|
||||
class="input input-primary flex h-auto items-center !shadow-lg"
|
||||
:class="!isValidQuery ? 'input-warning' : ''"
|
||||
>
|
||||
<div class="input input-primary flex items-center shadow-lg" :class="!isValidQuery ? 'input-warning' : ''">
|
||||
<mdi:magnify />
|
||||
<input
|
||||
class="input input-ghost w-72 flex-1"
|
||||
@@ -57,7 +54,7 @@ onMounted(() => {
|
||||
onUnmounted(() => resetSearch());
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
<style scoped>
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
transition: all 200ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
|
||||
@@ -2,16 +2,10 @@
|
||||
<ScrollableView :scrollable="scrollable" v-if="service.name">
|
||||
<template #header>
|
||||
<div class="mx-2 flex items-center gap-2 md:ml-4">
|
||||
<div class="flex flex-1 gap-1.5 truncate @container md:gap-2">
|
||||
<div class="inline-flex font-mono text-sm">
|
||||
<div class="font-semibold">{{ service.name }}</div>
|
||||
</div>
|
||||
<Tag class="mobile-hidden hidden font-mono @3xl:block" size="small">
|
||||
{{ $t("label.container", service.containers.length) }}
|
||||
</Tag>
|
||||
</div>
|
||||
<ph:stack-simple />
|
||||
<ContainerDropdown :containers="service.containers">{{ service.name }}</ContainerDropdown>
|
||||
<MultiContainerStat class="ml-auto" :containers="service.containers" />
|
||||
<MultiContainerActionToolbar class="mobile-hidden" @clear="viewer?.clear()" />
|
||||
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
@@ -20,7 +14,6 @@
|
||||
:stream-source="useServiceStream"
|
||||
:entity="service"
|
||||
:visible-keys="new Map<string[], boolean>()"
|
||||
:show-container-name="true"
|
||||
/>
|
||||
</template>
|
||||
</ScrollableView>
|
||||
@@ -41,5 +34,8 @@ const store = useSwarmStore();
|
||||
const { services } = storeToRefs(store) as unknown as { services: Ref<Service[]> };
|
||||
const service = computed(() => services.value.find((s) => s.name === name) ?? new Service("", []));
|
||||
|
||||
provideLoggingContext(toRef(() => service.value.containers));
|
||||
provideLoggingContext(
|
||||
toRef(() => service.value.containers),
|
||||
{ showContainerName: true, showHostname: false },
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -1,40 +1,57 @@
|
||||
<template>
|
||||
<div v-if="ready" data-testid="side-menu">
|
||||
<Toggle v-model="showSwarm" v-if="services.length > 0 || customGroups.length > 0">
|
||||
<div class="text-lg font-light">{{ $t("label.swarm-mode") }}</div>
|
||||
</Toggle>
|
||||
|
||||
<SlideTransition :slide-right="showSwarm">
|
||||
<template #left>
|
||||
<HostMenu />
|
||||
</template>
|
||||
<template #right>
|
||||
<div v-if="ready" data-testid="side-menu" class="flex min-h-0 w-full flex-col">
|
||||
<Carousel v-model="selectedCard" class="flex-1">
|
||||
<CarouselItem v-if="config.mode === 'k8s'" :title="$t('label.k8s-menu')" id="k8s">
|
||||
<K8sMenu />
|
||||
</CarouselItem>
|
||||
<CarouselItem v-if="config.mode === 'swarm' && services.length > 0" :title="$t('label.swarm-menu')" id="swarm">
|
||||
<SwarmMenu />
|
||||
</template>
|
||||
</SlideTransition>
|
||||
</CarouselItem>
|
||||
<CarouselItem :title="$t('label.host-menu')" id="host">
|
||||
<HostMenu />
|
||||
</CarouselItem>
|
||||
<CarouselItem :title="$t('label.group-menu')" v-if="customGroups.length > 0" id="group">
|
||||
<GroupMenu />
|
||||
</CarouselItem>
|
||||
<CarouselItem v-if="config.mode !== 'swarm' && services.length > 0" :title="$t('label.swarm-menu')" id="swarm">
|
||||
<SwarmMenu />
|
||||
</CarouselItem>
|
||||
</Carousel>
|
||||
</div>
|
||||
<div role="status" class="flex animate-pulse flex-col gap-4" v-else>
|
||||
<div class="h-3 w-full rounded-full bg-base-content/50 opacity-50" v-for="_ in 9"></div>
|
||||
<div class="bg-base-content/50 h-3 w-full rounded-full opacity-50" v-for="_ in 9"></div>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeRouteLeave } from "vue-router";
|
||||
const containerStore = useContainerStore();
|
||||
const { ready } = storeToRefs(containerStore);
|
||||
|
||||
const route = useRoute();
|
||||
const swarmStore = useSwarmStore();
|
||||
const { services, customGroups } = storeToRefs(swarmStore);
|
||||
|
||||
const showSwarm = useSessionStorage<boolean>("DOZZLE_SWARM_MODE", false);
|
||||
let defaultCard: "host" | "swarm" | "group" | "k8s";
|
||||
switch (config.mode) {
|
||||
case "k8s":
|
||||
defaultCard = "k8s";
|
||||
break;
|
||||
case "swarm":
|
||||
defaultCard = "swarm";
|
||||
break;
|
||||
default:
|
||||
defaultCard = "host";
|
||||
}
|
||||
const selectedCard = ref<"host" | "swarm" | "group" | "k8s">(defaultCard);
|
||||
|
||||
onBeforeRouteLeave((to) => {
|
||||
if (to.meta.swarmMode) {
|
||||
showSwarm.value = true;
|
||||
} else if (to.meta.containerMode) {
|
||||
showSwarm.value = false;
|
||||
}
|
||||
});
|
||||
watch(
|
||||
route,
|
||||
() => {
|
||||
if (route.meta.menu && ["host", "swarm", "group", "k8s"].includes(route.meta.menu as string)) {
|
||||
selectedCard.value = route.meta.menu as "host" | "swarm" | "group" | "k8s";
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
<style scoped lang="postcss"></style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
<template>
|
||||
<aside class="fixed h-screen w-[inherit] overflow-auto p-3" data-testid="navigation">
|
||||
<aside class="fixed flex h-screen w-[inherit] flex-col gap-4 p-3" data-testid="navigation">
|
||||
<h1>
|
||||
<router-link :to="{ name: '/' }">
|
||||
<LogoWithText class="logo h-16 w-40" />
|
||||
<router-link :to="{ name: '/' }" class="flex w-full items-center gap-4 overflow-hidden text-4xl font-thin">
|
||||
<Logo class="h-14 w-14 shrink-0" />
|
||||
Dozzle
|
||||
</router-link>
|
||||
|
||||
<small class="mb-4 block text-xs font-light" v-if="hostname">
|
||||
{{ hostname }}
|
||||
</small>
|
||||
<small class="mt-4 block text-sm font-light" v-if="hostname">{{ hostname }}</small>
|
||||
</h1>
|
||||
|
||||
<button
|
||||
class="input input-sm mt-4 inline-flex cursor-pointer items-center gap-2 font-light hover:border-primary"
|
||||
class="input input-sm hover:border-primary mt-2 inline-flex w-auto cursor-pointer items-center gap-2 self-start font-light"
|
||||
@click="$emit('search')"
|
||||
:title="$t('tooltip.search')"
|
||||
data-testid="search"
|
||||
@@ -21,23 +20,13 @@
|
||||
<key-shortcut char="k" class="text-base-content/70"></key-shortcut>
|
||||
</button>
|
||||
|
||||
<side-menu class="mt-4"></side-menu>
|
||||
<SideMenu class="flex-1" />
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import LogoWithText from "@/logo-text.svg";
|
||||
import Logo from "@/logo.svg";
|
||||
const { hostname } = config;
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
.logo {
|
||||
:deep(.content-fill) {
|
||||
@apply fill-base-content;
|
||||
}
|
||||
|
||||
:deep(.secondary-fill) {
|
||||
@apply fill-secondary;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -2,19 +2,15 @@
|
||||
<ScrollableView :scrollable="scrollable" v-if="stack.name">
|
||||
<template #header>
|
||||
<div class="mx-2 flex items-center gap-2 md:ml-4">
|
||||
<div class="flex flex-1 gap-1.5 truncate @container md:gap-2">
|
||||
<div class="inline-flex font-mono text-sm">
|
||||
<div class="font-semibold">{{ stack.name }}</div>
|
||||
</div>
|
||||
<Tag class="mobile-hidden hidden font-mono @3xl:block" size="small">
|
||||
<div class="@container flex flex-1 items-center gap-1.5 md:gap-2">
|
||||
<ph:stack />
|
||||
<div class="font-mono text-sm font-semibold">{{ stack.name }}</div>
|
||||
<ContainerDropdown :containers="stack.containers">
|
||||
{{ $t("label.container", stack.containers.length) }}
|
||||
</Tag>
|
||||
<Tag class="mobile-hidden hidden font-mono @3xl:block" size="small">
|
||||
{{ $t("label.serivce", stack.services.length) }}
|
||||
</Tag>
|
||||
</ContainerDropdown>
|
||||
</div>
|
||||
<MultiContainerStat class="ml-auto" :containers="stack.containers" />
|
||||
<MultiContainerActionToolbar class="mobile-hidden" @clear="viewer?.clear()" />
|
||||
<MultiContainerActionToolbar class="max-md:hidden" @clear="viewer?.clear()" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
@@ -23,7 +19,6 @@
|
||||
:stream-source="useStackStream"
|
||||
:entity="stack"
|
||||
:visible-keys="new Map<string[], boolean>()"
|
||||
:show-container-name="true"
|
||||
/>
|
||||
</template>
|
||||
</ScrollableView>
|
||||
@@ -42,5 +37,8 @@ const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
|
||||
const store = useSwarmStore();
|
||||
const { stacks } = storeToRefs(store) as unknown as { stacks: Ref<Stack[]> };
|
||||
const stack = computed(() => stacks.value.find((s) => s.name === name) ?? new Stack("", [], []));
|
||||
provideLoggingContext(toRef(() => stack.value.containers));
|
||||
provideLoggingContext(
|
||||
toRef(() => stack.value.containers),
|
||||
{ showContainerName: true, showHostname: false },
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -1,26 +1,46 @@
|
||||
<template>
|
||||
<ul class="menu p-0 text-[0.95rem]">
|
||||
<div class="mb-2 flex items-center">
|
||||
<div class="flex-1">
|
||||
{{ $t("label.service", services.length) }}
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<div class="dropdown dropdown-end dropdown-hover">
|
||||
<label tabindex="0" class="btn btn-square btn-ghost btn-sm">
|
||||
<ph:dots-three-vertical-bold />
|
||||
</label>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 shadow-sm"
|
||||
>
|
||||
<li>
|
||||
<a class="text-sm capitalize" @click="collapseAll()">
|
||||
<material-symbols-light:collapse-all class="w-4" />
|
||||
{{ $t("label.collapse-all") }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="menu w-full p-0 text-[0.95rem]" ref="menu">
|
||||
<li v-for="{ name, services } in stacks" :key="name">
|
||||
<details open>
|
||||
<summary class="font-light text-base-content/80">
|
||||
<summary class="text-base-content/80 font-light">
|
||||
<ph:stack />
|
||||
{{ name }}
|
||||
{{ name }} ({{ services.length }})
|
||||
|
||||
<router-link
|
||||
:to="{ name: '/stack/[name]', params: { name } }"
|
||||
class="btn btn-square btn-outline btn-primary btn-xs"
|
||||
active-class="btn-active"
|
||||
:title="$t('tooltip.merge-services')"
|
||||
:title="$t('tooltip.merge-all')"
|
||||
>
|
||||
<ph:arrows-merge />
|
||||
</router-link>
|
||||
</summary>
|
||||
<ul>
|
||||
<li v-for="service in services" :key="service.name">
|
||||
<router-link
|
||||
:to="{ name: '/service/[name]', params: { name: service.name } }"
|
||||
active-class="active-primary"
|
||||
>
|
||||
<router-link :to="{ name: '/service/[name]', params: { name: service.name } }" active-class="menu-active">
|
||||
<ph:stack-simple />
|
||||
<div class="truncate">
|
||||
{{ service.name }}
|
||||
@@ -31,18 +51,15 @@
|
||||
</details>
|
||||
</li>
|
||||
|
||||
<li v-if="serivcesWithoutStacks.length > 0">
|
||||
<li v-if="servicesWithoutStacks.length > 0">
|
||||
<details open>
|
||||
<summary class="font-light text-base-content/80">
|
||||
<summary class="text-base-content/80 font-light">
|
||||
<ph:circles-four />
|
||||
{{ $t("label.services") }}
|
||||
{{ $t("label.services") }} ({{ servicesWithoutStacks.length }})
|
||||
</summary>
|
||||
<ul>
|
||||
<li v-for="service in serivcesWithoutStacks" :key="service.name">
|
||||
<router-link
|
||||
:to="{ name: '/service/[name]', params: { name: service.name } }"
|
||||
active-class="active-primary"
|
||||
>
|
||||
<li v-for="service in servicesWithoutStacks" :key="service.name">
|
||||
<router-link :to="{ name: '/service/[name]', params: { name: service.name } }" active-class="menu-active">
|
||||
<ph:stack-simple />
|
||||
<div class="truncate">
|
||||
{{ service.name }}
|
||||
@@ -52,37 +69,20 @@
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
|
||||
<li v-if="customGroups.length > 0">
|
||||
<details open>
|
||||
<summary class="font-light text-base-content/80">
|
||||
<ph:bounding-box-fill />
|
||||
{{ $t("label.custom-groups") }}
|
||||
</summary>
|
||||
<ul>
|
||||
<li v-for="group in customGroups" :key="group.name">
|
||||
<router-link :to="{ name: '/group/[name]', params: { name: group.name } }" active-class="active-primary">
|
||||
<ph:stack-simple />
|
||||
<div class="truncate">
|
||||
{{ group.name }}
|
||||
</div>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const store = useSwarmStore();
|
||||
|
||||
const { stacks, services, customGroups } = storeToRefs(store);
|
||||
const { stacks, services } = storeToRefs(store);
|
||||
|
||||
const serivcesWithoutStacks = computed(() => services.value.filter((service) => !service.stack));
|
||||
const servicesWithoutStacks = computed(() => services.value.filter((service) => !service.stack));
|
||||
|
||||
const menu = useTemplateRef("menu");
|
||||
|
||||
const collapseAll = () => {
|
||||
const details = menu.value?.querySelectorAll("details");
|
||||
details?.forEach((detail) => (detail.open = false));
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="postcss">
|
||||
.menu {
|
||||
@apply text-[0.95rem];
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<aside class="flex h-[calc(100svh-50px)] flex-col gap-2">
|
||||
<header class="flex items-center gap-4">
|
||||
<material-symbols:terminal class="size-8" />
|
||||
<h1 class="text-2xl max-md:hidden">{{ container.name }}</h1>
|
||||
<h2 class="text-sm">Started <RelativeTime :date="container.created" /></h2>
|
||||
</header>
|
||||
|
||||
<div ref="host" class="shell flex-1"></div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Container } from "@/models/Container";
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
const { container, action } = defineProps<{ container: Container; action: "attach" | "exec" }>();
|
||||
|
||||
const { Terminal } = await import("@xterm/xterm");
|
||||
const { WebLinksAddon } = await import("@xterm/addon-web-links");
|
||||
const { FitAddon } = await import("@xterm/addon-fit");
|
||||
|
||||
const host = useTemplateRef<HTMLDivElement>("host");
|
||||
const terminal = new Terminal({
|
||||
cursorBlink: true,
|
||||
cursorStyle: "block",
|
||||
theme: {
|
||||
background: "rgba(0, 0, 0, 0)",
|
||||
},
|
||||
});
|
||||
terminal.loadAddon(new WebLinksAddon());
|
||||
const fitAddon = new FitAddon();
|
||||
terminal.loadAddon(fitAddon);
|
||||
|
||||
let ws: WebSocket | null = null;
|
||||
|
||||
function sendEvent(type: "userinput" | "resize", data?: string, width?: number, height?: number) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
|
||||
const event: { type: string; data?: string; width?: number; height?: number } = { type };
|
||||
if (data !== undefined) event.data = data;
|
||||
if (width !== undefined) event.width = width;
|
||||
if (height !== undefined) event.height = height;
|
||||
|
||||
ws.send(JSON.stringify(event));
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
terminal.open(host.value!);
|
||||
fitAddon.fit();
|
||||
|
||||
ws = new WebSocket(withBase(`/api/hosts/${container.host}/containers/${container.id}/${action}`));
|
||||
ws.onopen = () => {
|
||||
terminal.writeln(`Attaching to ${container.name} 🚀`);
|
||||
|
||||
// Send initial resize event
|
||||
sendEvent("resize", undefined, terminal.cols, terminal.rows);
|
||||
|
||||
if (action === "attach") {
|
||||
sendEvent("userinput", "\r");
|
||||
}
|
||||
|
||||
terminal.onData((data) => {
|
||||
sendEvent("userinput", data);
|
||||
});
|
||||
|
||||
// Handle terminal resize
|
||||
terminal.onResize(({ cols, rows }) => {
|
||||
sendEvent("resize", undefined, cols, rows);
|
||||
});
|
||||
|
||||
terminal.focus();
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => terminal.write(event.data);
|
||||
ws.addEventListener("close", () => {
|
||||
terminal.writeln("⚠️ Connection closed");
|
||||
});
|
||||
|
||||
// Handle window resize
|
||||
const { width, height } = useWindowSize();
|
||||
watch([width, height], () => {
|
||||
requestAnimationFrame(() => {
|
||||
fitAddon.fit();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
console.log("Closing WebSocket");
|
||||
terminal.dispose();
|
||||
ws?.close();
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
@reference "@/main.css";
|
||||
|
||||
.shell {
|
||||
& :deep(.terminal) {
|
||||
@apply overflow-hidden rounded border p-2;
|
||||
&:is(.focus) {
|
||||
@apply border-primary;
|
||||
}
|
||||
}
|
||||
|
||||
& :deep(.xterm-viewport) {
|
||||
@apply bg-base-200!;
|
||||
}
|
||||
|
||||
& :deep(.xterm-rows) {
|
||||
@apply text-base-content;
|
||||
}
|
||||
|
||||
& :deep(.xterm-cursor-block.xterm-cursor-blink) {
|
||||
animation-name: blink !important;
|
||||
}
|
||||
|
||||
& :deep(.xterm-selection) {
|
||||
@apply bg-primary/30;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% {
|
||||
background-color: var(--color-base-content);
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-color: inherit;
|
||||
color: var(--color-base-content);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div class="flex min-h-0 flex-col gap-2">
|
||||
<div class="flex min-h-0 flex-1 flex-col overflow-auto overscroll-y-contain">
|
||||
<div
|
||||
ref="container"
|
||||
class="scrollbar-hide flex shrink-0 grow snap-x snap-mandatory overflow-x-auto overscroll-x-contain scroll-smooth"
|
||||
>
|
||||
<component v-for="(card, index) in providedCards" :key="index" :is="card" ref="cards" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<h3 class="text-center text-sm font-thin">
|
||||
{{ cards?.[activeIndex].title }}
|
||||
</h3>
|
||||
<div class="flex flex-none justify-center gap-2" v-if="providedCards.length > 1">
|
||||
<button
|
||||
v-for="(c, index) in providedCards"
|
||||
:key="c.props?.id"
|
||||
@click="scrollToItem(index)"
|
||||
:class="[
|
||||
'size-2 cursor-pointer rounded-full transition-all duration-700',
|
||||
activeIndex === index ? 'bg-primary scale-125' : 'bg-base-content/50 hover:bg-base-content',
|
||||
]"
|
||||
:aria-label="c.props?.title"
|
||||
:title="c.props?.title"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import CarouselItem from "./CarouselItem.vue";
|
||||
const container = useTemplateRef<HTMLDivElement>("container");
|
||||
const activeIndex = ref(0);
|
||||
const activeId = defineModel<string>();
|
||||
const slots = defineSlots<{ default(): VNode[] }>();
|
||||
const providedCards = computed(() => slots.default().filter(({ type }) => type === CarouselItem));
|
||||
const cards = useTemplateRef<InstanceType<typeof CarouselItem>[]>("cards");
|
||||
|
||||
const scrollToItem = (index: number) => {
|
||||
cards.value?.[index].$el.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
inline: "start",
|
||||
});
|
||||
};
|
||||
|
||||
const { pause, resume } = watchPausable(activeId, (v) => {
|
||||
if (activeId.value) {
|
||||
const index = cards.value?.map((c) => c.id).indexOf(activeId.value) ?? -1;
|
||||
if (index !== -1) {
|
||||
console.log("watching", activeId.value);
|
||||
scrollToItem(index);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
watchOnce(cards, () => {
|
||||
if (activeId.value) {
|
||||
const index = cards.value?.map((c) => c.id).indexOf(activeId.value) ?? -1;
|
||||
if (index !== -1) {
|
||||
scrollToItem(index);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useIntersectionObserver(
|
||||
cards as Ref<InstanceType<typeof CarouselItem>[]>,
|
||||
(entries) => {
|
||||
entries.forEach(({ isIntersecting, target }) => {
|
||||
if (isIntersecting) {
|
||||
const index = cards.value?.map((c) => c.$el).indexOf(target as HTMLDivElement) ?? -1;
|
||||
if (index !== -1) {
|
||||
pause();
|
||||
activeIndex.value = index;
|
||||
activeId.value = cards.value?.[index].id;
|
||||
nextTick(() => resume());
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
root: container,
|
||||
threshold: 0.5,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.scrollbar-hide {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="w-full min-w-full shrink-0 snap-start snap-always p-0.5">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const { id, title } = defineProps<{ id: string; title?: string }>();
|
||||
|
||||
defineExpose({
|
||||
id,
|
||||
title,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="inline-flex gap-2">
|
||||
<time :datetime="date.toISOString()" class="mobile-hidden">{{ dateStr }}</time>
|
||||
<time :datetime="date.toISOString()" class="max-md:hidden">{{ dateStr }}</time>
|
||||
<time :datetime="date.toISOString()">{{ timeStr }}</time>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<template>
|
||||
<time :datetime="date.toISOString()" data-ci-skip>{{ text }}</time>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { formatDistanceToNow } from "date-fns/formatDistanceToNow";
|
||||
import { formatDistanceToNowStrict } from "date-fns/formatDistanceToNowStrict";
|
||||
|
||||
const {
|
||||
date,
|
||||
strict = false,
|
||||
suffix = true,
|
||||
} = defineProps<{
|
||||
date: Date;
|
||||
strict?: boolean;
|
||||
suffix?: boolean;
|
||||
}>();
|
||||
|
||||
const text = ref<string>();
|
||||
|
||||
watch($$(date), updateFromNow, { immediate: true });
|
||||
|
||||
function updateFromNow() {
|
||||
const fn = strict ? formatDistanceToNowStrict : formatDistanceToNow;
|
||||
text.value = fn(date, {
|
||||
addSuffix: suffix,
|
||||
});
|
||||
}
|
||||
useIntervalFn(updateFromNow, 30_000, { immediateCallback: true });
|
||||
</script>
|
||||
@@ -5,7 +5,7 @@
|
||||
</label>
|
||||
<div
|
||||
tabindex="0"
|
||||
class="dropdown-content z-50 mt-1 min-w-52 rounded-box border border-base-content/20 bg-base p-2 shadow"
|
||||
class="dropdown-content rounded-box border-base-content/20 bg-base-200 z-50 mt-1 min-w-52 border p-2 shadow-sm"
|
||||
>
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<template>
|
||||
<details class="dropdown" ref="details" v-on-click-outside="close">
|
||||
<details class="dropdown dropdown-end" ref="details" v-on-click-outside="close">
|
||||
<summary class="btn btn-primary flex-nowrap" v-bind="$attrs">
|
||||
<slot name="trigger"> {{ label }} <carbon:caret-down /></slot>
|
||||
</summary>
|
||||
<ul class="menu dropdown-content z-50 mt-1 w-52 rounded-box border border-base-content/20 bg-base p-2 shadow">
|
||||
<ul
|
||||
class="menu dropdown-content rounded-box border-base-content/20 bg-base-200 z-50 mt-1 max-h-72 w-48 flex-nowrap overflow-auto border p-2 shadow-sm"
|
||||
>
|
||||
<slot>
|
||||
<li v-for="item in options">
|
||||
<a @click="modelValue = item.value">
|
||||
<a @click="update(item.value as T)">
|
||||
<mdi:check class="w-4" v-if="modelValue == item.value" />
|
||||
<div v-else class="w-4"></div>
|
||||
{{ item.label }}
|
||||
@@ -24,17 +26,19 @@ type DropdownItem = {
|
||||
value: T;
|
||||
};
|
||||
|
||||
const { modelValue } = defineModels<{
|
||||
modelValue: T;
|
||||
}>();
|
||||
const model = defineModel<T>();
|
||||
|
||||
const { options = [], defaultLabel = "" } = defineProps<{
|
||||
options?: DropdownItem[];
|
||||
const { options, defaultLabel = "" } = defineProps<{
|
||||
options: DropdownItem[];
|
||||
defaultLabel?: string;
|
||||
}>();
|
||||
|
||||
const label = computed(() => options.find((item) => item.value === modelValue.value)?.label ?? defaultLabel);
|
||||
const label = computed(() => options.find((item) => item.value === model.value)?.label ?? defaultLabel);
|
||||
const details = ref<HTMLElement | null>(null);
|
||||
const close = () => details.value?.removeAttribute("open");
|
||||
watch(modelValue, () => close());
|
||||
|
||||
const update = (value: T) => {
|
||||
model.value = value;
|
||||
close();
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<mdi:satellite-variant v-if="type == 'agent'" />
|
||||
<ph:globe-simple v-else-if="type == 'remote'" />
|
||||
<mdi:hexagon-multiple v-else-if="type == 'swarm'" />
|
||||
<carbon:logo-kubernetes v-else-if="type == 'k8s'" />
|
||||
<ph:computer-tower v-else />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="animate-background h-1 w-1/2 bg-radial to-transparent to-75%" :class="colorClass"></div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
const { color = "primary" } = defineProps<{ color: "primary" | "error" | "secondary" }>();
|
||||
|
||||
const colorClass = computed(() => {
|
||||
switch (color) {
|
||||
case "primary":
|
||||
return "from-primary";
|
||||
case "error":
|
||||
return "from-error";
|
||||
case "secondary":
|
||||
return "from-secondary";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.animate-background {
|
||||
animation: gradient-animation 3s ease-out infinite;
|
||||
}
|
||||
|
||||
@keyframes gradient-animation {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0%);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<label class="label cursor-pointer gap-4">
|
||||
<div class="label-text"><slot name="label" /></div>
|
||||
<label class="label text-base-content cursor-pointer gap-4">
|
||||
<div class="flex-1 whitespace-normal"><slot name="label" /></div>
|
||||
<slot name="input" />
|
||||
</label>
|
||||
</template>
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
<template>
|
||||
<nav class="fixed top-0 z-10 w-full border-b border-base-content/20 bg-base p-2" data-testid="navigation">
|
||||
<div class="flex items-center">
|
||||
<router-link :to="{ name: '/' }">
|
||||
<Logo class="logo h-8" />
|
||||
</router-link>
|
||||
<nav class="border-base-content/20 bg-base-200 pt-safe fixed top-0 z-10 w-full border-b" data-testid="navigation">
|
||||
<div class="p-2">
|
||||
<div class="flex items-center">
|
||||
<router-link :to="{ name: '/' }">
|
||||
<Logo class="h-10" />
|
||||
</router-link>
|
||||
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<a class="btn btn-circle flex" @click="$emit('search')" :title="$t('tooltip.search')">
|
||||
<mdi:magnify class="size-5" />
|
||||
</a>
|
||||
<label class="btn btn-circle swap swap-rotate" data-testid="hamburger">
|
||||
<input type="checkbox" v-model="show" />
|
||||
<mdi:close class="swap-on" />
|
||||
<mdi:hamburger-menu class="swap-off" />
|
||||
</label>
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<a class="btn btn-circle flex" @click="$emit('search')" :title="$t('tooltip.search')">
|
||||
<mdi:magnify class="size-5" />
|
||||
</a>
|
||||
<label class="btn btn-circle swap swap-rotate" data-testid="hamburger">
|
||||
<input type="checkbox" v-model="show" />
|
||||
<mdi:close class="swap-on" />
|
||||
<mdi:hamburger-menu class="swap-off" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<div v-show="show" class="flex h-[calc(100svh-55px)]">
|
||||
<SideMenu class="flex-1" />
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<div v-show="show" class="h-[calc(100vh-60px)] overflow-auto">
|
||||
<SideMenu />
|
||||
</div>
|
||||
</transition>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
@@ -34,18 +36,15 @@ watch(route, () => {
|
||||
show.value = false;
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="postcss">
|
||||
li.exited {
|
||||
@apply opacity-50;
|
||||
}
|
||||
|
||||
<style scoped>
|
||||
@reference "@/main.css";
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
@apply transition-opacity;
|
||||
}
|
||||
|
||||
.fade-enter-active .menu,
|
||||
.fade-leave-active .menu {
|
||||
.fade-enter-active > div,
|
||||
.fade-leave-active > div {
|
||||
@apply transition-transform;
|
||||
}
|
||||
|
||||
@@ -54,14 +53,8 @@ li.exited {
|
||||
@apply opacity-0;
|
||||
}
|
||||
|
||||
.fade-enter-from .menu,
|
||||
.fade-leave-to .menu {
|
||||
@apply -translate-y-2;
|
||||
}
|
||||
|
||||
.logo {
|
||||
:deep(.secondary-fill) {
|
||||
@apply fill-secondary;
|
||||
}
|
||||
.fade-enter-from > div,
|
||||
.fade-leave-to > div {
|
||||
@apply -translate-y-10;
|
||||
}
|
||||
</style>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user