Compare commits

...

407 Commits

Author SHA1 Message Date
Amir Raminfar 3fe087935c chore: release v6.2.4 2024-02-10 09:46:49 -08:00
renovate[bot] b47cf7940a chore(deps): update all non-major dependencies (#2771)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-10 17:16:41 +00:00
Amir Raminfar 420b398e50 chore: uses 1.22 for testing (#2770) 2024-02-10 17:13:19 +00:00
Ivan 91d79c99ea feat: add Content-Encoding for downloading logs (#2769) 2024-02-10 06:02:10 -08:00
Amir Raminfar 28c2cdc571 chore: release v6.2.3 2024-02-09 11:13:20 -08:00
Amir Raminfar d67f2da1ba fix: uses github.com/goccy/go-json for less CPU (#2766) 2024-02-09 11:11:20 -08:00
Ahmet a99672a5da chore: adjust supported env variables in readme (#2768) 2024-02-08 23:18:09 +00:00
renovate[bot] eefa472516 fix(deps): update all non-major dependencies (#2764)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-08 18:58:12 +00:00
Amir Raminfar 11cde3d5fc chore: fixes improvements with go1.22 2024-02-07 12:33:28 -08:00
Amir Raminfar 935d76e539 chore: release v6.2.2 2024-02-07 11:20:47 -08:00
Amir Raminfar 2497d62fe2 chore: fixes tests 2024-02-07 11:13:33 -08:00
Amir Raminfar 7103cc3c5c fix: fixes initital load not being ready yet (#2763) 2024-02-07 18:53:19 +00:00
Amir Raminfar cd877c910c fix: fixes a possible race condition (#2762) 2024-02-07 17:48:48 +00:00
renovate[bot] 946f59e603 fix(deps): update all non-major dependencies (#2758)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-07 16:29:05 +00:00
Amir Raminfar a03e5d10aa chore: disables static check (#2760) 2024-02-07 16:24:02 +00:00
renovate[bot] 183326db52 fix(deps): update all non-major dependencies (#2754)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-05 01:57:41 +00:00
Amir Raminfar 0ca3058f7b chore: release v6.2.1 2024-02-04 09:03:07 -08:00
Amir Raminfar 9c7ccc0a59 fix: stops streaming of stats for new containers when stopped (#2753) 2024-02-04 16:28:39 +00:00
Amir Raminfar 85d989450d chore: uses xsync package for maps (#2752) 2024-02-04 16:25:05 +00:00
Amir Raminfar 3f81f355bb chore: release v6.2.0 2024-02-03 14:08:26 -08:00
Amir Raminfar 25cb3c4c53 chore: adds tests for store (#2751) 2024-02-03 09:06:05 -08:00
Amir Raminfar 1d4c162fba Revert "chore: updates tests"
This reverts commit 19a83b7b62.
2024-02-02 16:48:58 -08:00
Amir Raminfar 19a83b7b62 chore: updates tests 2024-02-02 16:20:08 -08:00
Amir Raminfar 454156ae2c fix: fixes a regression bug in map. fixes #2749 (#2750) 2024-02-02 15:54:00 -08:00
EDIflyer 2d8b263df4 fix: reword message on similar container switch countdown banner (#2748) 2024-02-02 14:42:39 -08:00
Amir Raminfar fc8b67a902 fix: fixes race issues (#2747) 2024-02-02 13:56:48 -08:00
Amir Raminfar b8869784d4 fix: fixes a potential deadlock when reading stats (#2745) 2024-02-02 09:57:06 -08:00
Amir Raminfar 099c053341 feat: sorts most recent containers on top for fuzzy search (#2746) 2024-02-02 09:41:26 -08:00
Amir Raminfar bd86b2289b feat: updates labels between running and all containers (#2742) 2024-02-01 22:07:16 +00:00
Amir Raminfar 8677a34087 feat: collects all stats like cpu and mem in background for up to 5 minutes (#2740) 2024-02-01 12:43:44 -08:00
renovate[bot] 2c398cc227 chore(deps): update all non-major dependencies (#2741)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-01 20:29:39 +00:00
Amir Raminfar bb0ca30b81 feat: search textbox is now draggable (#2737) 2024-01-30 17:22:58 +00:00
renovate[bot] f9eb8186d1 chore(deps): update dependency @types/node to ^20.11.10 (#2735)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-29 00:16:05 +00:00
Amir Raminfar d767be3757 chore: updates go modules (#2734) 2024-01-28 19:06:49 +00:00
renovate[bot] 86f9839211 chore(deps): update all non-major dependencies (#2733)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-27 18:52:22 +00:00
Amir Raminfar 67f25b7bb0 chore: renames cts to cjs (#2732) 2024-01-25 10:03:46 -08:00
Amir Raminfar 025ce28134 chore: release v6.1.1 2024-01-24 10:27:28 -08:00
renovate[bot] 25fced7bb9 chore(deps): update all non-major dependencies (#2731)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-24 17:52:53 +00:00
Amir Raminfar ef636c86fc fix: removes deprecated code (#2730) 2024-01-24 17:48:29 +00:00
Amir Raminfar 808abd57bb fix: improves logfmt (#2729) 2024-01-23 13:08:03 -08:00
Amir Raminfar d07dc2e983 fix: fixes cpu on popup being too long (#2728) 2024-01-23 09:01:15 -08:00
renovate[bot] 099f778ca2 fix(deps): update all non-major dependencies (#2724)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-22 16:32:02 +00:00
renovate[bot] 9a760a2738 chore(deps): update dependency jsdom to v24 (#2723)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-22 08:23:33 -08:00
renovate[bot] b53c6c8275 fix(deps): update dependency @iconify-json/carbon to ^1.1.28 (#2722)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-22 00:46:05 +00:00
renovate[bot] 8dbe9a5774 chore(deps): update all non-major dependencies (#2720)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-20 22:33:32 +00:00
Amir Raminfar 5d28917d11 fix(deps): update module github.com/docker/docker to v25 (#2719)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-19 14:43:18 -08:00
renovate[bot] 586d717aff fix(deps): update all non-major dependencies (#2717)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-19 16:32:24 +00:00
Amir Raminfar 8aba7debe8 chore: release v6.1.0 2024-01-18 09:11:38 -08:00
renovate[bot] c6647133dd chore(deps): update all non-major dependencies to ^3.4.15 (#2713)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-18 17:08:47 +00:00
Amir Raminfar 82765f4b68 feat: adds support for compact mode. fixes #2710 (#2712) 2024-01-18 17:04:58 +00:00
Amir Raminfar 0c33d0f921 feat: defaults locale to auto (#2711) 2024-01-18 08:56:38 -08:00
Amir Raminfar 89fe9e6b91 feat: adds a dropdown to switch language locale (#2708) 2024-01-18 01:22:06 +00:00
Amir Raminfar ef359d339c feat: adds support for LogFmt. fixes #2695 (#2705) 2024-01-17 14:01:40 -08:00
Amir Raminfar 9d2a8af435 feat: makes button for page size smaller (#2707) 2024-01-17 14:01:18 -08:00
renovate[bot] 6f96303f1d chore(deps): update all non-major dependencies (#2709)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-17 21:54:37 +00:00
Amir Raminfar 48d7109079 Revert "feat: adds support for LogFmt. fixes #2695"
This reverts commit ccd7f0dd86.
2024-01-16 13:39:04 -08:00
Amir Raminfar ccd7f0dd86 feat: adds support for LogFmt. fixes #2695 2024-01-16 13:30:57 -08:00
Amir Raminfar 390d1a3062 chore: release v6.0.8 2024-01-15 10:39:24 -08:00
Amir Raminfar c6c2c13404 feat: adds option to home page to show more than default containers per page (#2703) 2024-01-15 18:38:21 +00:00
renovate[bot] f80dd56bdf chore(deps): update all non-major dependencies (#2702)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-15 02:22:22 +00:00
Amir Raminfar 89babeab9a chore: moves warn to debug for releases 2024-01-12 11:37:38 -08:00
Feriman 9abab4ce77 read only docker.sock (#2698) 2024-01-12 11:35:49 -08:00
Feriman 19c3daf2f7 read only docker.sock (#2699) 2024-01-12 11:35:31 -08:00
AmadeusGraves 04fb4f3fa4 Update locale es.yml (#2690) 2024-01-11 05:29:01 -08:00
Amir Raminfar c5680a9b3f chore: makes faked data better. (#2689) 2024-01-10 13:42:23 -08:00
Amir Raminfar 5cd6fc1243 chore: release v6.0.7 2024-01-10 11:26:24 -08:00
Amir Raminfar 1e2ce61991 fix: fixes title bar alignment to base of text (#2688) 2024-01-10 11:23:53 -08:00
renovate[bot] b5e78ae979 fix(deps): update all non-major dependencies (#2687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-10 19:15:56 +00:00
Amir Raminfar f7a36483f9 feat: moves search box higher. fixes #2679 (#2684) 2024-01-10 11:10:24 -08:00
Amir Raminfar cc6ae1d63c fix: fixes 24 hour style being broken on chrome. fixes #2680 (#2685) 2024-01-10 11:10:10 -08:00
Amir Raminfar 9cb973f30e fix: moves cache for releases to API. fixes #2681 (#2686) 2024-01-10 11:07:09 -08:00
renovate[bot] c98bcd3fcf chore(deps): update all non-major dependencies (#2677)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-10 00:18:05 +00:00
Amir Raminfar b0cc0a0377 chore: release v6.0.6 2024-01-09 14:09:34 -08:00
Amir Raminfar 3a705f2de0 fix: fixes title container stats in small screen (#2678) 2024-01-09 14:09:16 -08:00
Amir Raminfar e1d38a3582 fix: fixes stats on windows (#2675) 2024-01-09 13:03:30 -08:00
Amir Raminfar da24ca249a chore: enables container queries (#2676) 2024-01-09 11:49:56 -08:00
Amir Raminfar 6ca33b5a62 feat: alt-click on side menu opens a new panel (#2674) 2024-01-09 10:25:54 -08:00
Amir Raminfar 9a431dbcb0 fix: adds long message for preview (#2672) 2024-01-08 15:53:02 -08:00
Amir Raminfar 3b5370fbae chore: release v6.0.5 2024-01-08 14:40:09 -08:00
Amir Raminfar 8b183e98ff feat: allows for date format to be overriden with locale. fixes #2665 (#2671) 2024-01-08 14:36:08 -08:00
renovate[bot] 7038bb35b2 chore(deps): update all non-major dependencies (#2670)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-08 20:25:25 +00:00
Amir Raminfar 72a580574a feat: adds preview for settings (#2669) 2024-01-08 12:21:37 -08:00
renovate[bot] 27fe0663cf chore(deps): update all non-major dependencies (#2668)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-08 01:20:43 +00:00
Amir Raminfar 00cb7a6018 chore: release v6.0.4 2024-01-07 14:21:56 -08:00
Amir Raminfar 89b763af59 feat: if password hash is != 64 then error is displayed. (#2667) 2024-01-07 22:17:05 +00:00
Amir Raminfar 71d76e9228 chore: release v6.0.3 2024-01-06 09:00:43 -08:00
renovate[bot] d8d7c9fd4f fix(deps): update all non-major dependencies (#2663)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-06 16:32:46 +00:00
Amir Raminfar f87d3bbe3e chore: improves icon size (#2662) 2024-01-06 16:29:13 +00:00
renovate[bot] de88a25bcc chore(deps): update all non-major dependencies (#2661)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-06 03:31:24 +00:00
Amir Raminfar fc93e2d33c chore: adds more tests (#2660) 2024-01-05 19:28:02 -08:00
Amir Raminfar c1eb699dc6 feat: moves settings to top of the home page (#2659) 2024-01-06 01:56:29 +00:00
Amir Raminfar 77517a76da chore: release v6.0.2 2024-01-05 13:07:04 -08:00
Amir Raminfar aac38d954d fix: fixes broken enter on fuzy dialog (#2658) 2024-01-05 21:01:21 +00:00
renovate[bot] d5724d2652 fix(deps): update all non-major dependencies (#2657)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-05 20:50:50 +00:00
Amir Raminfar eafd2f144f fix: improves performance for fuzzy search dialog (#2656) 2024-01-05 12:47:07 -08:00
Amir Raminfar 589cad54d9 fix: makes minor improvements to fuzzy search dialog (#2653) 2024-01-04 15:12:47 -08:00
renovate[bot] 3a8054f68b chore(deps): update all non-major dependencies to ^3.4.5 (#2652)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-04 15:52:20 +00:00
Amir Raminfar a4026e16e8 feat: adds labels to fuzzy search dialog (#2649) 2024-01-04 07:47:51 -08:00
Amir Raminfar ee4f94ce2e fix: fixes z-index on scroll progress component (#2651) 2024-01-03 23:16:05 +00:00
renovate[bot] 942f05e8d8 chore(deps): update pnpm to v8.14.0 (#2650)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-03 21:33:11 +00:00
Amir Raminfar cc67ee94f5 chore: updates modules 2024-01-03 11:57:58 -08:00
Amir Raminfar f4dd232259 chore: release v6.0.1 2024-01-02 14:18:41 -08:00
Amir Raminfar a730eb9660 chore: fixes docs 2024-01-02 14:14:14 -08:00
Amir Raminfar f43f4d4ee4 chore: cleans up docs for env vars 2024-01-02 14:08:31 -08:00
Amir Raminfar bcb3c326e8 docs: swamp sections for authentication docs 2024-01-02 13:58:55 -08:00
Amir Raminfar 3e72a7c2ff fix: fixes authentication documentation 2024-01-02 06:57:02 -08:00
Amir Raminfar 78eceb2237 chore: fixes typo 2024-01-01 15:20:51 -08:00
Amir Raminfar ef5954d7cf feat: caches releases for up to an hour (#2646) 2024-01-01 15:11:59 -08:00
Amir Raminfar 66b9f8ab45 chore: release v6.0.0 2024-01-01 13:38:38 -08:00
Amir Raminfar 45cf78c256 fix: removes extra slash for healthcheck. Fixes #2643 (#2645) 2024-01-01 13:37:00 -08:00
Ankit R Gadiya ed43889e3b feat: add support for custom headers for forward-proxy auth (#2642) 2023-12-31 20:59:14 -08:00
renovate[bot] 77789aca73 chore(deps): update all non-major dependencies (#2641)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-01 00:14:57 +00:00
Amir Raminfar 6eb356aaa9 feat!: removes legacy authentication model (#2633) 2023-12-31 22:05:41 +00:00
Amir Raminfar cb6adf275d fix: adds timestamp to logs 2023-12-31 08:09:47 -08:00
Amir Raminfar 4bd66601cb docs: removes custom content link in docs 2023-12-30 18:53:00 -08:00
Amir Raminfar 98bf45c5bc feat!: removes custom content help support (#2628) 2023-12-30 14:18:14 -08:00
renovate[bot] 943cfab0e5 chore(deps): update all non-major dependencies (#2638)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-30 17:07:06 +00:00
Amir Raminfar 1835404376 chore: cleans up snapshot 2023-12-29 19:16:13 -08:00
renovate[bot] 38c84710f1 chore(deps): update all non-major dependencies (#2637)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-30 03:14:27 +00:00
renovate[bot] e73ae6cbb7 chore(deps): update dependency @vitejs/plugin-vue to v5.0.1 (#2635)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-29 21:05:26 +00:00
Amir Raminfar 45fa933f12 test: add e2e tests for simple auth mode (#2634) 2023-12-29 17:29:15 +00:00
Amir Raminfar feaf9c35d4 chore: removes ref-macros 2023-12-29 08:22:32 -08:00
renovate[bot] b7dba3851d chore(deps): update all non-major dependencies (#2632)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-29 16:10:55 +00:00
Amir Raminfar 86bdf747da chore: release v5.8.1 2023-12-28 13:54:23 -08:00
Amir Raminfar 06bf8a312f feat: displays warning for legacy auth (#2631) 2023-12-28 13:51:05 -08:00
Amir Raminfar 48b0a64c7a chore: release v5.8.0 2023-12-28 07:30:44 -08:00
Amir Raminfar 0ab9c730c7 feat!: strips all ansi colors and disables showing any colors (#2626) 2023-12-28 07:27:52 -08:00
renovate[bot] 15fc01d265 chore(deps): update all non-major dependencies (#2629)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-28 04:51:26 +00:00
Amir Raminfar 6a4c384eb6 Revert "feat!: removes custom content help support"
This reverts commit 904b2a4f89.
2023-12-27 20:41:58 -08:00
Amir Raminfar 904b2a4f89 feat!: removes custom content help support 2023-12-27 19:08:04 -08:00
renovate[bot] a65dcbadb7 chore(deps): update dependency @vitejs/plugin-vue to v5 (#2625) 2023-12-26 21:15:40 +00:00
renovate[bot] 5ef251d021 chore(deps): update all non-major dependencies (#2624)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-26 19:23:16 +00:00
Javadz 681d286f5c docs: Update authentication.md (#2623) 2023-12-25 06:32:08 -08:00
renovate[bot] bfa33f0c35 fix(deps): update dependency daisyui to ^4.4.24 (#2622)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-25 01:15:56 +00:00
Akash Ramaswamy 7a0fc84842 fix: copy log ansi character parsing and html decode (#2621) 2023-12-23 06:48:22 -08:00
renovate[bot] 9e5b6a88e2 fix(deps): update all non-major dependencies (#2620)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-22 23:08:38 +00:00
renovate[bot] 3402b050d5 fix(deps): update all non-major dependencies (#2618)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-21 16:29:06 +00:00
Amir Raminfar 1d2e97b513 chore: updates go modules 2023-12-20 18:59:05 -08:00
Amir Raminfar a8c9237c72 fix: improves padding for mobile (#2616) 2023-12-20 22:42:30 +00:00
Amir Raminfar 3a214590be chore: uses size-x instead of w-x h-x (#2615) 2023-12-20 11:56:48 -08:00
renovate[bot] f46900ec28 fix(deps): update dependency @intlify/unplugin-vue-i18n to v2 (#2613)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-20 09:47:42 -08:00
renovate[bot] b1068fdaf8 fix(deps): update all non-major dependencies (#2612)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-20 05:06:26 +00:00
Amir Raminfar c88c1ed643 fix: fixes login page height by using dynamic dvh (#2611) 2023-12-19 20:45:40 -08:00
renovate[bot] 56cb2ab346 chore(deps): update actions/deploy-pages action to v4 (#2609)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-19 17:20:49 -08:00
renovate[bot] b9f6828703 chore(deps): update actions/upload-pages-artifact action to v3 (#2610)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-19 17:20:40 -08:00
renovate[bot] 0ed8192cae fix(deps): update all non-major dependencies (#2608)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-20 01:02:30 +00:00
Amir Raminfar aea1b67a7f chore: updates date-fns (#2607) 2023-12-19 16:37:36 +00:00
renovate[bot] 0def791638 fix(deps): update all non-major dependencies (#2605)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-19 16:33:26 +00:00
Amir Raminfar f84013ffb8 chore: release v5.7.3 2023-12-18 08:24:07 -08:00
Amir Raminfar 0db179c312 fix: uses username instead of email to validate remote user proxy. see #2598 (#2604) 2023-12-18 08:21:56 -08:00
renovate[bot] 5a4432244c chore(deps): update dependency @types/node to ^20.10.5 (#2603)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-18 01:29:58 +00:00
Amir Raminfar 6b09fc4ab7 fix: escapes avatar url correctly. see #2601 (#2602) 2023-12-17 14:26:12 -08:00
Amir Raminfar 0cf728473b chore: release v5.7.2 2023-12-16 14:10:20 -08:00
renovate[bot] 729b1d9ce2 chore(deps): update all non-major dependencies (#2600)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-16 22:07:09 +00:00
Amir Raminfar 4b658fbf1c fix: proxies avatars which should fix #2591 and remove all CSP errors (#2599) 2023-12-16 14:01:53 -08:00
Amir Raminfar 83746fdce0 chore: release v5.7.1 2023-12-15 10:18:56 -08:00
renovate[bot] 8778d67174 fix(deps): update all non-major dependencies (#2595)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-15 18:16:26 +00:00
renovate[bot] becfc9e8d5 chore(deps): update actions/upload-artifact action to v4 (#2596)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-15 10:12:58 -08:00
Amir Raminfar f332b5c9b5 chore: updates icons 2023-12-15 10:12:24 -08:00
Amir Raminfar 0f3093ae61 fix: generates an avatar even if email is missing (#2594) 2023-12-14 13:11:13 -08:00
Amir Raminfar 7306a4eeb1 feat: updates users if users.yml is changed without needing to restart dozzle (#2593) 2023-12-14 13:05:53 -08:00
Amir Raminfar 937638dce5 chore: fixes component file (#2588) 2023-12-11 14:29:06 -08:00
renovate[bot] 21078e79e8 chore(deps): update all non-major dependencies (#2586)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-11 01:23:06 +00:00
EDIflyer 1c8b9f50d7 docs: Update supported-env-vars.md (#2585) 2023-12-10 13:39:44 -08:00
Amir Raminfar 9cd388642c chore: release v5.7.0 2023-12-10 13:19:36 -08:00
Amir Raminfar 3bd97f98b2 fix: disables hover on mobile (#2583) 2023-12-10 13:13:55 -08:00
Akash Ramaswamy d776777b7e feat: Moves Jump-To-Context right (#2584) 2023-12-10 13:13:29 -08:00
Amir Raminfar 8baa5021a6 feat: adds some translations (#2582) 2023-12-09 22:40:11 +00:00
renovate[bot] 65d464541e fix(deps): update all non-major dependencies (#2581)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-09 22:06:29 +00:00
Akash Ramaswamy 5d883c0825 fix: actions on log UI issues (#2580) 2023-12-09 14:02:24 -08:00
Akash Ramaswamy e6fa8ced58 feat: actions on log messages (#2572) 2023-12-09 05:56:51 -08:00
Gregory D. Estes cdf9005b6f docs: Update supported-env-vars.md (#2578) 2023-12-08 18:16:12 -08:00
renovate[bot] 89aa10d4b7 fix(deps): update all non-major dependencies (#2577)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-08 22:12:09 +00:00
renovate[bot] caa68df169 chore(deps): update actions/setup-go action to v5 (#2574)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-07 11:30:21 -08:00
renovate[bot] e2230d5adc fix(deps): update all non-major dependencies (#2573)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-07 18:32:10 +00:00
Amir Raminfar 2fbb65c921 fix: fixes profile disk configuration when pinned is an empty set (#2570) 2023-12-07 08:05:19 -08:00
Amir Raminfar 185538b794 test: fixes test snapshot and updates vitest (#2569) 2023-12-05 23:00:29 +00:00
renovate[bot] 3bb2a509f2 chore(deps): update actions/configure-pages action to v4 (#2566)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-05 14:58:47 -08:00
renovate[bot] cf43d36500 chore(deps): update actions/deploy-pages action to v3 (#2567)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-05 14:58:36 -08:00
renovate[bot] 7819b3fec3 chore(deps): update all non-major dependencies (#2565)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-05 21:33:35 +00:00
Amir Raminfar 0d6c9305c7 test: fixes snapshots (#2564) 2023-12-05 08:09:02 -08:00
ab623 24f298b94d fix: Updates to sidebar on mobile to align closer to desktop (#2562) 2023-12-05 07:53:10 -08:00
Akash Ramaswamy f240b7011b test: added test cases for container actions (#2559) 2023-12-05 07:48:37 -08:00
renovate[bot] f027c753c1 fix(deps): update all non-major dependencies (#2560)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-04 02:26:54 +00:00
Amir Raminfar 5c5b4bb622 chore: release v5.6.1 2023-12-01 11:27:57 -08:00
Amir Raminfar 7da60edb1a chore(analytics): removes GA and only pings with beacon. also adds support actions (#2557) 2023-12-01 18:26:41 +00:00
ab623 23fbf361ce fix: Icon placement when hovering over containers with health checks. (#2556) 2023-12-01 10:13:18 -08:00
Amir Raminfar 6dab94d3b4 fix: changes --enableactions to --enable-actions (#2554) 2023-12-01 06:32:49 -08:00
Amir Raminfar fd5ed091d5 fix: moves favorite star to the left and healthcheck to the right of container name (#2555) 2023-12-01 06:31:26 -08:00
Akash Ramaswamy 3d198bf6c1 docs: actions feature (#2552) 2023-11-30 16:20:55 -08:00
Amir Raminfar 4806b63ea8 chore: release v5.6.0 2023-11-30 15:19:49 -08:00
renovate[bot] c2ee3d50c9 chore(deps): update all non-major dependencies (#2550)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-30 23:07:27 +00:00
Amir Raminfar 10e4a7cfaa fix: disables action routes if not configured (#2549) 2023-11-30 22:28:42 +00:00
Akash Ramaswamy f78534f529 feat: can start and stop containers now using the drop down. This feature is not enabled by default. (#2548) 2023-11-30 14:04:22 -08:00
ab623 050e499f8e fix: Added .gitkeep to dist folder, to enable fresh build with no errors (#2547) 2023-11-29 09:33:36 -08:00
Amir Raminfar 29e1bbc981 chore: release v5.5.0 2023-11-28 15:55:34 -08:00
Amir Raminfar 1fa44cc920 chore: updates modules (#2544) 2023-11-28 15:53:31 -08:00
Amir Raminfar 23aaf99947 chore: minor perf improvement 2023-11-28 14:31:37 -08:00
Amir Raminfar 60650ddc2c feat: syncs all profile localStorage to disk (#2537) 2023-11-27 15:57:54 -08:00
Amir Raminfar b54b419a08 fix: removes pages content from index.html (#2541) 2023-11-26 19:41:24 -08:00
Amir Raminfar 9bc5496b0e fix: imroves margin left on title bar (#2542) 2023-11-26 19:41:11 -08:00
renovate[bot] 2016f8d8b0 fix(deps): update all non-major dependencies (#2538)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-27 00:59:58 +00:00
renovate[bot] 72c6382d73 chore(deps): update dependency jsdom to v23 (#2539)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-26 16:56:25 -08:00
Amir Raminfar ae72e3f4a6 chore: updates icons (#2540) 2023-11-26 16:55:26 -08:00
Amir Raminfar 1ec25e6a64 chore: release v5.4.5 2023-11-25 13:29:18 -08:00
Amir Raminfar ee4f344346 chore: updates imports 2023-11-25 13:29:07 -08:00
renovate[bot] 06e05bca45 chore(deps): update all non-major dependencies (#2534)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-25 21:26:11 +00:00
Amir Raminfar 5635ba4619 fix: improves secondary color (#2533) 2023-11-25 13:22:23 -08:00
Amir Raminfar 32dec4e817 fix: improves primary colors (#2531) 2023-11-25 11:47:37 -08:00
Amir Raminfar 2ed41282a5 fix: uses a single key in localStorage to save all visible JSON keys. (#2530) 2023-11-23 18:37:00 -08:00
Ahmet 26cd16a403 fix: update de.yml to use consistent wording and fix small issues (#2528) 2023-11-22 09:26:31 -08:00
Alban Browaeys b243783f5d fix: make README container host port consistent (#2529) 2023-11-22 05:50:48 -08:00
Amir Raminfar 8ab71e2caa feat: improves search style and adding a slight animation (#2527) 2023-11-22 05:48:26 -08:00
Amir Raminfar 318b07d1a9 feat: when doing modifier+f again in search, hides search and show browser search (#2526) 2023-11-22 05:48:03 -08:00
renovate[bot] 7ff26b822f chore(deps): update all non-major dependencies (#2525)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-21 22:28:56 +00:00
Amir Raminfar cacf2cd261 chore: release v5.4.4 2023-11-21 14:19:11 -08:00
Amir Raminfar 47bc4b7fc5 fix: fixes bugs with reporting on addr and base (#2521) 2023-11-21 14:14:54 -08:00
renovate[bot] 38c4d9e4b7 chore(deps): update all non-major dependencies (#2517)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-20 00:54:55 +00:00
Amir Raminfar 1d87d09e89 chore: release v5.4.3 2023-11-19 14:18:50 -08:00
Amir Raminfar 70645be134 fix: fixes security warnings with net package (#2516) 2023-11-19 05:14:37 +00:00
Amir Raminfar c659f103f8 feat: improves analytics by removing google analytics in favor of a simple ping (#2515) 2023-11-19 03:35:24 +00:00
renovate[bot] 58fef4e16a chore(deps): update all non-major dependencies (#2514)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-18 15:57:55 +00:00
Amir Raminfar 70e0c359da fix: hides errors when trying to parse simple numbers as json. fixes #2510 (#2512) 2023-11-18 07:52:27 -08:00
Amir Raminfar 0ad8f32588 chore: moves warn log to debug 2023-11-17 15:43:41 -08:00
Amir Raminfar 07147d1e5c fix: returns afters healthcheck 2023-11-17 15:38:17 -08:00
EDIflyer 02a40fdd8e docs: Add docker-compose.yml option (#2511) 2023-11-17 12:48:43 -08:00
renovate[bot] eb94da7b92 chore(deps): update all non-major dependencies (#2507)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-17 03:27:35 +00:00
Amir Raminfar 660b4c6f51 chore: updates to vite v5 (#2505) 2023-11-16 19:24:05 -08:00
renovate[bot] f08518d442 chore(deps): update all non-major dependencies (#2502)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-15 16:47:45 +00:00
Amir Raminfar b066433933 chore: release v5.4.2 2023-11-14 13:06:04 -08:00
Amir Raminfar 52dc1ff2d1 fix: fixes collapse button to not cover the menu on left. see #2500 (#2501) 2023-11-14 13:05:01 -08:00
Suresh Ramakrishnaiah 10ea7cfb51 docs: update authentication.md (#2492) 2023-11-13 18:41:25 -08:00
renovate[bot] eea9813f84 fix(deps): update all non-major dependencies (#2498)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-14 00:52:30 +00:00
Amir Raminfar 8ac79c707c chore: updates go version for CI (#2499) 2023-11-14 00:48:28 +00:00
Amir Raminfar 4637b49075 chore: fixes golang version 2023-11-13 14:29:21 -08:00
renovate[bot] 147902f191 fix(deps): update all non-major dependencies (#2493)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-13 14:28:59 -08:00
Amir Raminfar f83b5c948e fix: converts HSL colors OKLCH with DaisyUI v4 (#2497) 2023-11-13 14:00:35 -08:00
Amir Raminfar 542c895812 chore: fixes padding 2023-11-11 14:08:11 -08:00
Amir Raminfar d14d140cb6 chore(test): adds test for simple mode (#2491) 2023-11-11 22:04:45 +00:00
Amir Raminfar be74d48de7 fix: improves the alignment of drop downs 2023-11-11 13:48:14 -08:00
Amir Raminfar f8a9396f6e chore(test): adds test for proxy mode (#2490) 2023-11-11 13:22:06 -08:00
Amir Raminfar 009529f20e chore: release v5.4.1 2023-11-11 06:41:45 -08:00
Amir Raminfar 63f16ef575 fix: fixes proxy pass not finding user (#2488) 2023-11-11 06:37:06 -08:00
Amir Raminfar 9de964dac4 chore: release v5.4.0 2023-11-10 15:20:41 -08:00
Amir Raminfar 1ff0b5541d chore: fixes incorrect version in doc 2023-11-10 13:47:30 -08:00
Amir Raminfar 55326cdd41 docs: adds sponsor section (#2487) 2023-11-10 21:08:32 +00:00
Amir Raminfar 9fa4911aec feat: list of releases can now be seen at the top (#2480) 2023-11-10 19:45:47 +00:00
Amir Raminfar 7dae54df26 chore: fixes golang version 2023-11-10 09:04:30 -08:00
renovate[bot] 1b101e62da chore(deps): update all non-major dependencies (#2485)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-10 09:04:07 -08:00
Amir Raminfar 5c5cd17fb6 chore: removes netlify files 2023-11-10 08:46:27 -08:00
Amir Raminfar c697be659b docs: adds docs for custom content (#2483) 2023-11-08 14:58:48 -08:00
Amir Raminfar d469066c07 feat: moves user's name and email to dropdown (#2479) 2023-11-07 11:07:34 -08:00
Amir Raminfar 4380681812 feat: supports default avatar when gravatar is missing (#2478) 2023-11-07 09:26:48 -08:00
Amir Raminfar 75cf2f045a chore: release v5.3.1 2023-11-06 15:29:56 -08:00
Amir Raminfar ab5807b3ae chore: fixes go.mod 2023-11-06 15:24:50 -08:00
renovate[bot] fe0f163090 chore(deps): update all non-major dependencies (#2477)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-06 15:23:04 -08:00
Amir Raminfar 38d1f99387 fix: reduces padding on mobile (#2476) 2023-11-06 21:59:53 +00:00
Amir Raminfar 11298e83b0 chore: release v5.3.0 2023-11-06 12:39:29 -08:00
Amir Raminfar 32de313c27 feat: supports having custom content with markdown (#2466) 2023-11-06 12:39:11 -08:00
renovate[bot] 1e1c64d6e7 fix(deps): update all non-major dependencies (#2474)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-06 01:13:20 +00:00
Amir Raminfar 0f02de2070 chore: release v5.2.3 2023-11-05 12:30:54 -08:00
Amir Raminfar bb085affb2 fix: removes fetch request when user does not exist (#2473) 2023-11-05 12:29:44 -08:00
Amir Raminfar 1d022b51b5 fix: fixes bad icon (#2472) 2023-11-04 20:53:24 +00:00
Amir Raminfar eb31b46e6f fix: fixes a potential rare edge case that log entry has no payload. see #2451. (#2471) 2023-11-04 13:51:29 -07:00
Amir Raminfar 066541288a docs: updates readme (#2470) 2023-11-03 21:10:21 +00:00
renovate[bot] eba7d92ba6 fix(deps): update all non-major dependencies (#2468)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-03 17:44:40 +00:00
Jacek Dobosz 579ae12920 docs: installation on podman (#2467) 2023-11-03 05:41:48 -07:00
Amir Raminfar ba9531dc59 chore: release v5.2.2 2023-11-01 12:36:12 -07:00
Amir Raminfar c4cc233720 chore: updates go.mod 2023-11-01 12:13:47 -07:00
Amir Raminfar 6c1f59c7e3 feat: removes request analytics on page load (#2464) 2023-11-01 12:13:17 -07:00
Amir Raminfar ebc02900b6 feat: prints local docker engine error 2023-10-31 16:02:58 -07:00
Amir Raminfar 4e5f7ae1cf chore: release v5.2.1 2023-10-31 15:11:58 -07:00
Amir Raminfar 55f076382e fix: fixes not being able to create /data dir (#2463) 2023-10-31 15:11:15 -07:00
Amir Raminfar cae8f2740d feat: warns against using username and password 2023-10-31 12:45:30 -07:00
Amir Raminfar c75b5a598d chore: fixes typo 2023-10-31 12:33:43 -07:00
Amir Raminfar 701f16fcc2 chore: release v5.2.0 2023-10-31 12:33:12 -07:00
Amir Raminfar 49d53ce255 chore(docs): updates gh pages to have simple auth 2023-10-31 12:30:39 -07:00
Amir Raminfar e57d038221 fix: changes mime type for fetching between to application/x-jsonl. see #2458 (#2461) 2023-10-31 11:22:27 -07:00
Amir Raminfar c74384d012 fix: fixes blank name for proxy mode 2023-10-31 11:15:50 -07:00
Amir Raminfar 5a068133bf fix: fixes forward proxy bug not working (#2460) 2023-10-30 22:42:40 +00:00
Amir Raminfar e85a4aefa2 feat: adds support for user.yml from disk (#2455) 2023-10-30 12:20:00 -07:00
Amir Raminfar b0e43333d2 chore: release v5.1.2 2023-10-30 08:20:25 -07:00
renovate[bot] d2c71e2dc9 fix(deps): update dependency vue-i18n to ^9.6.2 (#2459)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-30 03:56:24 +00:00
renovate[bot] 7fdaa3e4f0 chore(deps): update actions/setup-node action to v4 (#2457)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-29 18:50:14 -07:00
renovate[bot] 0344e2adb4 chore(deps): update actions/checkout action to v4 (#2456)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-29 18:49:54 -07:00
Amir Raminfar 7431c3455f chore(docs): enables gh pages 2023-10-27 13:00:51 -07:00
Amir Raminfar 1fe46e605a feat: syncs settings to disk for authenticated users (#2445) 2023-10-27 12:53:42 -07:00
renovate[bot] e4d1a18d37 fix(deps): update all non-major dependencies (#2453)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-27 19:44:15 +00:00
Amir Raminfar a0a2b7175c chore: release v5.1.1 2023-10-27 11:22:01 -07:00
Amir Raminfar b88fc3c5d1 feat: makes hour time format always 2 digits (#2452) 2023-10-27 09:46:04 -07:00
Amir Raminfar a8bc1dd4c2 chore: reverts netlify changes 2023-10-26 13:45:09 -07:00
Amir Raminfar 29fcb53fac chore: uses removes store 2023-10-26 13:34:38 -07:00
Amir Raminfar 4dfd57612d chore: rollback pnpm changes 2023-10-26 11:28:43 -07:00
Amir Raminfar 51b9ad5f00 chore: tries to clear pnpm store prune again 2023-10-26 10:29:21 -07:00
Amir Raminfar f4b53fd09d chore: tries to clear pnpm store prune 2023-10-26 10:28:49 -07:00
renovate[bot] 5d5628530b fix(deps): update dependency fuse.js to v7 (#2447)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-26 09:47:36 -07:00
Amir Raminfar 4d0904bb2c chore: cleans pnpm 2023-10-26 09:41:27 -07:00
renovate[bot] 13ebc06812 fix(deps): update all non-major dependencies (#2446)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-25 22:09:14 +00:00
Amir Raminfar 7fbf760e9a chore: release v5.1.0 2023-10-25 14:01:40 -07:00
Amir Raminfar 9a2935d3d4 chore: fixes typos 2023-10-25 13:56:56 -07:00
Amir Raminfar fd24659903 chore(docs): adds docks for forward-proxy 2023-10-25 13:48:11 -07:00
renovate[bot] d2a43b7e6f chore(deps): update actions/setup-node action to v4 (#2444) 2023-10-24 06:43:12 -07:00
Amir Raminfar 2a07ca39db chore(refactor): refactors go code (#2443) 2023-10-24 06:28:16 -07:00
Amir Raminfar 74e44bd5f2 fix: fixes log levels that are not in the start of the line 2023-10-23 19:06:04 -07:00
Amir Raminfar be11c32139 feat: adds forward-proxy (#2442) 2023-10-23 15:31:39 -07:00
Amir Raminfar e55df82023 chore: release v5.0.10 2023-10-23 09:01:18 -07:00
Amir Raminfar 78c1d72f3d feat: adds more levels parsing like [ error] with spaces (#2441) 2023-10-23 09:00:29 -07:00
renovate[bot] caa3bb14ab chore(deps): update dependency vitepress to v1.0.0-rc.23 (#2440)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-23 00:58:43 +00:00
renovate[bot] 2881f25a1a chore(deps): update all non-major dependencies (#2435)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-21 16:39:44 +00:00
Amir Raminfar 11a972b048 fix: fixes broken icons (#2438) 2023-10-21 09:36:11 -07:00
Amir Raminfar 8fc308d294 chore: updates vue 2023-10-20 12:32:32 -07:00
renovate[bot] 33e7b84061 chore(deps): update node.js to v21 (#2436)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-20 12:28:45 -07:00
Amir Raminfar 470d6ed0d9 chore: release v5.0.9 2023-10-20 11:57:56 -07:00
Amir Raminfar 01f3c5ee15 fix: fixes reading incorrect number of bytes. see #2431 (#2434) 2023-10-20 11:51:06 -07:00
Amir Raminfar 79f8efd909 feat: supports lower case level like [info] (#2433) 2023-10-20 18:12:22 +00:00
Amir Raminfar 021d3f7a99 chore: updates node modules 2023-10-18 14:12:28 -07:00
Amir Raminfar bf79b213c6 chore(docs): update security readme. (#2430) 2023-10-18 12:31:03 -07:00
Amir Raminfar f216587c9b chore: release v5.0.8 2023-10-16 11:46:10 -07:00
Amir Raminfar 803b66188e chore: code clean up (#2428) 2023-10-16 11:45:43 -07:00
Amir Raminfar 37d2b07720 fix: reverts broken changes to event source (#2427) 2023-10-16 11:37:23 -07:00
Amir Raminfar 9879004ae3 feat: adds workflow for semantic messages 2023-10-16 10:11:14 -07:00
Amir Raminfar 0c5c08deeb chore: release v5.0.7 2023-10-16 10:01:06 -07:00
Peter Dave Hello 89a6fbfab2 Add zh-tw locale (#2425) 2023-10-16 09:59:29 -07:00
Amir Raminfar 5b1f3892a7 chore: release v5.0.6 2023-10-16 09:26:50 -07:00
Amir Raminfar 37c6033371 fix: cleans up reading messages when headers are invalid (#2424) 2023-10-16 09:26:27 -07:00
Amir Raminfar 4f748bf9e1 fix: fixes swallowed error when bad headers are found and fixing #2416 (#2423)
* fix: fixes swallowed error when bad headers are found and fixing #2416

* minor clean up

* fixes broken on first connect

* fixes on reconnect
2023-10-16 03:35:10 +00:00
renovate[bot] c0b7f960f4 chore(deps): update dependency lint-staged to v15 (#2421)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-15 20:11:28 -07:00
Amir Raminfar f35e2c0ace chore: code clean up 2023-10-15 19:24:38 -07:00
renovate[bot] 69e434f14b chore(deps): update pnpm to v8.9.2 2023-10-16 01:25:56 +00:00
Amir Raminfar f47f60e51c chore: code clean up 2023-10-15 16:47:16 -07:00
Peter Dave Hello 55775f651f Improve i18n support, close #2417 (#2418) 2023-10-15 16:45:53 -07:00
renovate[bot] 59c72f91ce chore(deps): update all non-major dependencies 2023-10-15 22:15:26 +00:00
Amir Raminfar 732df996bf fix(docs): fixes bad url in docs 2023-10-08 14:17:21 -07:00
Amir Raminfar a7f602449b chore: updates docs to have base 2023-10-08 13:52:34 -07:00
renovate[bot] 84c062c13d chore(deps): update all non-major dependencies 2023-10-08 20:24:12 +00:00
Amir Raminfar 194fcd0dea fix: clears all containers on reconnect and removes stale old ones (#2414) 2023-10-07 21:35:23 +00:00
Amir Raminfar ae1c0207de chore: cleans up v-model for toggle 2023-10-05 20:35:20 -07:00
renovate[bot] 3ca2308be8 chore(deps): update all non-major dependencies 2023-10-06 02:27:53 +00:00
Amir Raminfar bf000a1028 chore: release v5.0.5 2023-10-05 13:51:40 -07:00
Amir Raminfar 9b2cf1254e chore(docs): adds docs for hostname label 2023-10-05 13:50:41 -07:00
Amir Raminfar f4bc3bdc21 feat: uses --hostname as the label for localhost. see #2394 (#2412)
* feat: uses --hostname as the label for localhost. see #2394

* fixes tests
2023-10-05 13:36:33 -07:00
Amir Raminfar 0e135d0eac fix(docs): fixes broken docs page 2023-10-03 15:40:00 -07:00
Amir Raminfar 8f314f5e6f chore: release v5.0.4 2023-10-03 10:28:50 -07:00
Amir Raminfar f1dad96b86 fix: clears logs when an exception is thrown in logs events source (#2406)
* feat: adds more logging for eventsource

* feat: removes error from event streams

* removes unused code

* adds errors back

* moves logs to debug

* removes lastEventId

* fixes tests
2023-10-03 16:58:46 +00:00
renovate[bot] 32bc9a407b chore(deps): update all non-major dependencies to ^0.56.5 2023-10-03 15:12:27 +00:00
renovate[bot] a0e3baabbf chore(deps): update all non-major dependencies 2023-10-03 02:48:37 +00:00
Amir Raminfar b40b23b76f chore: updates node modules 2023-10-02 13:13:13 -07:00
renovate[bot] aa2a9dc7a0 chore(deps): update docker/metadata-action action to v5 (#2404)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-02 08:25:12 -07:00
renovate[bot] 6fe883f4be chore(deps): update docker/setup-buildx-action action to v3 (#2405)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-02 08:25:02 -07:00
Amir Raminfar 10721f5d07 feat: adds linux/arm/v6 to docker builds (#2401) 2023-09-30 13:46:46 -07:00
Amir Raminfar f6ef1f7381 feat: adds touch icon to index.html (#2402) 2023-09-30 13:46:34 -07:00
Amir Raminfar 32f9364f2b chore: cleans up package.json (#2397) 2023-09-29 21:06:52 +00:00
Amir Raminfar 233c546f92 chore: release v5.0.3 2023-09-28 11:05:50 -07:00
Amir Raminfar 666ad7ba05 fix: fixes negative CPU values when container stops 2023-09-28 09:46:45 -07:00
Amir Raminfar d3814455cb chore: release v5.0.2 2023-09-27 16:42:53 -07:00
Amir Raminfar 67dbbfce97 fix: improves event stream error and only shows when closed 2023-09-27 16:33:56 -07:00
Amir Raminfar a9afe7a299 fix: fixes bad translation 2023-09-27 16:21:59 -07:00
Amir Raminfar 00cac97114 chore: updates node modules 2023-09-27 16:02:35 -07:00
Amir Raminfar 58edb4f602 feat: adds automatic redirect when a new container is found (#2396)
* feat: implements a toast for alerting errors and other useful information

* removes unused code

* feat: adds automatic redirect when a new container is found

* complete the flow with alerts and settings page

* adds more langs and option for once

* removes files
2023-09-27 16:00:44 -07:00
Amir Raminfar c3b5991dc7 feat: implements a toast for alerting errors and other useful information (#2395)
* feat: implements a toast for alerting errors and other useful information

* removes unused code
2023-09-27 13:29:33 -07:00
Amir Raminfar 9751ce3cf5 chore: code clean up 2023-09-25 11:40:56 -07:00
Amir Raminfar 40d02d993b chore: code clean up 2023-09-25 11:37:07 -07:00
Amir Raminfar 889ebf3acb chore: release v5.0.1 2023-09-25 09:56:14 -07:00
Amir Raminfar a051245805 fix: UI improvements to colors 2023-09-25 09:54:16 -07:00
Amir Raminfar 66738313c8 fix: improves styles for redirect alert 2023-09-25 09:44:48 -07:00
renovate[bot] e8aa6adbda chore(deps): update docker/login-action action to v3 (#2392) 2023-09-24 18:53:33 -07:00
renovate[bot] 45ec71a6a1 chore(deps): update docker/build-push-action action to v5 (#2391) 2023-09-24 18:52:51 -07:00
Amir Raminfar 072a6c09bb feat: finds same name containers and shows option redirect to new containers (#2390) 2023-09-24 15:22:27 -07:00
Amir Raminfar c233549b4b chore: removes unused file 2023-09-23 14:07:52 -07:00
Amir Raminfar 4a30a2ba5e chore: release v5.0.0 2023-09-23 13:34:24 -07:00
Amir Raminfar 538a1e5dcd fix: fixes padding for settings page 2023-09-23 13:29:47 -07:00
Amir Raminfar 46ba01e3a8 chore: updates all other modules 2023-09-23 13:27:16 -07:00
Amir Raminfar 9074895653 chore: fixes tests 2023-09-23 13:22:52 -07:00
Amir Raminfar 9e30045e6f fix: Improves UI with padding and color improvements 2023-09-23 13:16:40 -07:00
Amir Raminfar ad461c9dbc chore: fixes tests 2023-09-23 13:06:01 -07:00
Amir Raminfar b150a6b772 fix: fixes no space between date and time 2023-09-23 12:14:49 -07:00
Amir Raminfar c123c62bf7 chore: release v5.0.0-beta.0 2023-09-22 13:54:00 -07:00
Amir Raminfar ccc42443c8 fix: fixes sorting on search dialog bug 2023-09-22 13:41:08 -07:00
Amir Raminfar 907360a334 chore: fixes tests 2023-09-22 13:17:33 -07:00
Amir Raminfar 3c4442a8dc feat: alt+enter in search opens a new pane 2023-09-22 13:15:22 -07:00
Amir Raminfar 9e98640731 chore: css clean up 2023-09-22 12:46:28 -07:00
Amir Raminfar fa47512771 fix: fixes broken links on settings page 2023-09-22 12:35:44 -07:00
Amir Raminfar e31e3138fb chore: creates a container context (#2388)
* chore: creates a container context

* fixes tests

* fixes tests
2023-09-22 12:24:05 -07:00
Amir Raminfar e2bb23b2ed fix: adds style for links in settings page 2023-09-22 11:37:14 -07:00
Amir Raminfar 60beb85bb1 fix: improves colors in light mode 2023-09-22 11:30:38 -07:00
Amir Raminfar b437850063 fix: adds custom scrollbars back 2023-09-22 11:03:34 -07:00
Amir Raminfar 9f3a256334 feat!: refactors UI using faster components and clean up visually (#2381)
* feat: moves to tailwindcss and better component library

* update styles

* creates toggle component

* adds drop down component

* cleans up components

* removes unused components

* uses tailwind for scroll view

* removes table component

* improves animation

* cleans up more styles

* uses more tailwind

* cleans up more styles with flex

* more styles

* removes bulma

* adds colors

* updates modules

* fixes bugs

* stops importing styles.scss

* more clean up

* cleans up headers

* cleans up title

* fixes title

* fixes mobile-hidden

* fixes shadow

* fixes colors

* add tailwindcss/nesting

* adds more colors

* fixes more colors

* updates colors

* fixes colors

* colors

* fixes menu on left

* menu and modal

* menu and modal

* fuzzy search

* fixes menu on left

* remove logs

* cleans up search

* adds host to search

* remove outline from inputs

* cleans up left search icon

* removes unused styles

* fixes docker

* removes sass!

* cleans up styles

* Fixe smobile menu

* fixes mobile menu

* fixes typecheck

* fixes seconday color

* adds drop down for container

* cleans header css

* updates css

* fixes other layouts

* updates some tests

* fixes border

* fixes home screen font

* fixes top header

* fixes tests

* fixes fieldlist

* fixes complex

* cleans up more

* removes index

* fixes tests

* fixes tests

* resolves conflicts
2023-09-22 10:59:29 -07:00
Amir Raminfar 2d30c8c529 chore: release v4.11.5 2023-09-22 10:24:19 -07:00
Amir Raminfar 8acd5794e8 fix: fixes crash when names are not found. see #2386 (#2387)
* fix: fixes crash when names are not found. see #2386

* adds snapshots
2023-09-22 10:24:03 -07:00
Amir Raminfar 2e01f3acb4 chore: adds keep label for issues 2023-09-20 14:23:46 -07:00
renovate[bot] 87951b0027 fix(deps): update all non-major dependencies (#2382)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-18 09:28:07 -07:00
renovate[bot] 552a9cf78f chore(deps): update docker/bake-action action to v4 (#2383)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-18 09:15:19 -07:00
Amir Raminfar 5b0f18dd28 fix(dep): rolling back dep causing breaking changes 2023-09-11 09:55:29 -07:00
renovate[bot] 0ac2ba4873 fix(deps): update dependency @intlify/unplugin-vue-i18n to v1 (#2379)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-11 09:02:56 -07:00
renovate[bot] cfba470c0e chore(deps): update actions/checkout action to v4 (#2378) 2023-09-10 19:10:47 -07:00
renovate[bot] 9e1de4f226 chore(deps): update all non-major dependencies 2023-09-11 01:53:23 +00:00
Amir Raminfar 0812bfaca4 chore: removes unused component 2023-09-09 16:47:19 -07:00
Amir Raminfar 45d8991c5f chore: cleans up deps 2023-09-05 09:31:07 -07:00
renovate[bot] 0f46d7ffd7 fix(deps): update all non-major dependencies 2023-09-04 01:15:17 +00:00
Amir Raminfar fcf60daafa chore: clean up types 2023-08-28 11:11:44 -07:00
Amir Raminfar 617bf71cad fix: fixes flicker on scroll progress bar (#2371) 2023-08-28 16:06:31 +00:00
renovate[bot] 06c6a605fd chore(deps): update all non-major dependencies 2023-08-28 00:20:18 +00:00
Amir Raminfar d41508d4e7 chore: release v4.11.4 2023-08-25 15:18:35 -07:00
Amir Raminfar a77db667ab fix: quick fix to improve the slash with single host 2023-08-25 14:21:20 -07:00
Amir Raminfar 34ad45c64e feat: improves host labels and respects configurations for labels (#2369)
* feat: improves host labels and respects configurations for labels

* fixes tests
2023-08-25 13:42:00 -07:00
Amir Raminfar 512b181b48 fix: adds ellipses for titles that have swarm id (#2368) 2023-08-25 13:33:03 -07:00
Amir Raminfar be5e496380 chore: updates node modules 2023-08-25 11:00:27 -07:00
202 changed files with 9534 additions and 6414 deletions
+1
View File
@@ -6,6 +6,7 @@ daysUntilClose: 3
exemptLabels:
- pinned
- security
- keep
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
+20 -19
View File
@@ -8,14 +8,14 @@ jobs:
name: JavaScript Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
name: Install Node
with:
node-version: latest
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: latest
cache: "pnpm"
@@ -29,11 +29,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: 1.20.x
go-version: 1.22.x
check-latest: true
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Run Go Tests with Coverage
run: make test SKIP_ASSET=1
int-test:
@@ -41,14 +42,14 @@ jobs:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
name: Install Node
with:
node-version: latest
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: latest
cache: "pnpm"
@@ -56,9 +57,9 @@ jobs:
- name: Install dependencies
run: pnpm install
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Build
uses: docker/bake-action@v3
uses: docker/bake-action@v4
with:
load: true
set: |
@@ -66,7 +67,7 @@ jobs:
*.cache-to=type=gha,mode=max
- name: Run Playwright tests
run: docker compose up --exit-code-from playwright
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
@@ -79,21 +80,21 @@ jobs:
steps:
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
images: amir20/dozzle
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.9.1
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
- name: Login to DockerHub
uses: docker/login-action@v2.2.0
uses: docker/login-action@v3.0.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v4.1.1
uses: docker/build-push-action@v5.1.0
with:
push: true
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }}
build-args: TAG=${{ steps.meta.outputs.version }}
labels: ${{ steps.meta.outputs.labels }}
@@ -105,11 +106,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
- name: Release to Github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+5 -5
View File
@@ -14,21 +14,21 @@ jobs:
steps:
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
images: amir20/dozzle
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.9.1
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
- name: Login to DockerHub
uses: docker/login-action@v2.2.0
uses: docker/login-action@v3.0.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v4.1.1
uses: docker/build-push-action@v5.1.0
with:
push: true
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }}
build-args: TAG=${{ steps.meta.outputs.version }}
labels: ${{ steps.meta.outputs.labels }}
+62
View File
@@ -0,0 +1,62 @@
# Sample workflow for building and deploying a VitePress site to GitHub Pages
#
name: Deploy VitePress site to Pages
on:
push:
branches: [master]
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: pages
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # Not needed if lastUpdated is not enabled
- uses: pnpm/action-setup@v2
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: latest
cache: pnpm # or pnpm / yarn
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Install dependencies
run: pnpm install
- name: Build with VitePress
run: |
pnpm docs:build
touch docs/.vitepress/dist/.nojekyll
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/.vitepress/dist
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
name: Deploy
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+20
View File
@@ -0,0 +1,20 @@
name: "Lint PR"
on:
pull_request_target:
types:
- opened
- edited
- synchronize
permissions:
pull-requests: read
jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+20 -15
View File
@@ -11,14 +11,14 @@ jobs:
name: Typecheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
name: Install Node
with:
node-version: latest
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: latest
cache: "pnpm"
@@ -31,14 +31,14 @@ jobs:
name: JavaScript Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
name: Install Node
with:
node-version: latest
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: latest
cache: "pnpm"
@@ -52,26 +52,31 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: 1.20.x
go-version: 1.22.x
check-latest: true
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Run Go Tests with Coverage
run: make test SKIP_ASSET=1
# - name: Stactic checker
# uses: dominikh/staticcheck-action@v1.3.0
# with:
# install-go: false
int-test:
name: Integration Tests
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
name: Install Node
with:
node-version: latest
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: latest
cache: "pnpm"
@@ -79,9 +84,9 @@ jobs:
- name: Install dependencies
run: pnpm install
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Build
uses: docker/bake-action@v3
uses: docker/bake-action@v4
with:
load: true
set: |
@@ -89,7 +94,7 @@ jobs:
*.cache-to=type=gha,mode=max
- name: Run Playwright tests
run: docker compose up --exit-code-from playwright
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
+1
View File
@@ -1,4 +1,5 @@
certs
/data
dist
node_modules
.cache
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
printWidth: 120,
plugins: ["prettier-plugin-tailwindcss"],
};
-3
View File
@@ -1,3 +0,0 @@
module.exports = {
printWidth: 120,
};
+1 -1
View File
@@ -1 +1 @@
-r '\.(go)$' -R 'node_modules' -G '\*\_test.go' -s -- go run main.go --level debug
-r '\.(go)$' -R 'node_modules' -G '\*\_test.go' -s -- go run -race main.go --level debug
+6 -8
View File
@@ -1,5 +1,5 @@
# Build assets
FROM --platform=$BUILDPLATFORM node:20-alpine as node
FROM --platform=$BUILDPLATFORM node:21-alpine as node
RUN corepack enable
@@ -14,7 +14,7 @@ COPY package.json ./
RUN pnpm install --offline --ignore-scripts --no-optional
# Copy assets and translations to build
COPY .* vite.config.ts ./
COPY .* *.config.ts *.config.js *.config.cjs ./
COPY assets ./assets
COPY locales ./locales
COPY public ./public
@@ -22,7 +22,7 @@ COPY public ./public
# Build assets
RUN pnpm build
FROM --platform=$BUILDPLATFORM golang:1.21.0-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.22.0-alpine AS builder
RUN apk add --no-cache ca-certificates && mkdir /dozzle
@@ -36,10 +36,7 @@ RUN go mod download
COPY --from=node /build/dist ./dist
# Copy all other files
COPY analytics ./analytics
COPY healthcheck ./healthcheck
COPY docker ./docker
COPY web ./web
COPY internal ./internal
COPY main.go ./
# Args
@@ -49,11 +46,12 @@ ARG TARGETOS TARGETARCH
# Build binary
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=$TAG" -o dozzle
RUN mkdir /data
FROM scratch
ENV PATH /bin
COPY --from=builder /data /data
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=builder /dozzle/dozzle /dozzle
+1 -1
View File
@@ -15,7 +15,7 @@ fake_assets:
.PHONY: test
test: fake_assets
go test -cover ./...
go test -cover -race ./...
.PHONY: build
build: dist
+57 -160
View File
@@ -15,29 +15,27 @@ https://user-images.githubusercontent.com/260667/227634771-9ebbe381-16a8-465a-b2
- Search logs using regex 🔦
- Small memory footprint 🏎
- Split screen for viewing multiple logs
- Download logs easy
- Download logs easily
- Live stats with memory and CPU usage
- Authentication with username and password 🚨
- Multi-user authentication with support for proxy forward authorization 🚨
Dozzle should work for most. It has been tested with hundreds of containers. However, it doesn't support offline searching. Products like [Loggly](https://www.loggly.com), [Papertrail](https://papertrailapp.com) or [Kibana](https://www.elastic.co/products/kibana) are more suited for full search capabilities.
Dozzle has been tested with hundreds of containers. However, it doesn't support offline searching. Products like [Loggly](https://www.loggly.com), [Papertrail](https://papertrailapp.com) or [Kibana](https://www.elastic.co/products/kibana) are more suited for full search capabilities.
Dozzle doesn't cost any money and aims to focus on real-time debugging.
## Getting Started
## Getting Dozzle
Dozzle is a very small Docker container (4 MB compressed). Pull the latest release from the index:
Dozzle is a small container (4 MB compressed). Pull the latest release with:
$ docker pull amir20/dozzle:latest
## Using Dozzle
### Running Dozzle
The simplest way to use dozzle is to run the docker container. Also, mount the Docker Unix socket with `--volume` to `/var/run/docker.sock`:
$ docker run --name dozzle -d --volume=/var/run/docker.sock:/var/run/docker.sock -p 8888:8080 amir20/dozzle:latest
$ docker run --name dozzle -d --volume=/var/run/docker.sock:/var/run/docker.sock:ro -p 8888:8080 amir20/dozzle:latest
Dozzle will be available at [http://localhost:8888/](http://localhost:8888/). You can change `-p 8888:8080` to any port. For example, if you want to view dozzle over port 4040 then you would do `-p 4040:8080`.
Dozzle will be available at [http://localhost:8888/](http://localhost:8888/).
### Connecting with Docker compose
Here is the Docker Compose file:
version: "3"
services:
@@ -45,182 +43,81 @@ Dozzle will be available at [http://localhost:8888/](http://localhost:8888/). Yo
container_name: dozzle
image: amir20/dozzle:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/run/docker.sock:/var/run/docker.sock:ro
ports:
- 9999:8080
- 8888:8080
### Connecting to remote hosts
For advanced options like [authentication](https://dozzle.dev/guide/authentication), [remote hosts](https://dozzle.dev/guide/remote-hosts) or common [questions](https://dozzle.dev/guide/faq) see documentation at [dozzle.dev](https://dozzle.dev/guide/getting-started).
Dozzle supports connecting to multiple remote hosts via `tcp://` using TLS or without. Appropriate certs need to be mounted for Dozzle to be able to successfully connect. At this point, `ssh://` is not supported because Dozzle docker image does not ship with any ssh clients.
## Technical Details
To configure remote hosts, `--remote-host` or `DOZZLE_REMOTE_HOST` need to provided and the `pem` files need to be mounted to `/cert` directory. The `/cert` directory expects to have `/certs/{ca,cert,key}.pem` or `/certs/{host}/{ca,cert,key}.pem` in case of multiple hosts.
Dozzle users automatic API negotiation which works with most Docker configurations. Dozzle also works with [Colima](https://github.com/abiosoft/colima) and [Podman](https://podman.io/).
Below are examples of using `--remote-host` via CLI:
### Installation on podman
$ docker run -v /var/run/docker.sock:/var/run/docker.sock -v /path/to/certs:/certs -p 8080:8080 amir20/dozzle --remote-host tcp://167.99.1.1:2376
By default Podman doesn't have a background process but you can enable this for Dozzle to work.
Multiple `--remote-host` flags can be used to specify multiple hosts.
Verify first if your podman installation has enabled remote socket:
Or to use compose:
```
podman info
```
version: "3"
services:
dozzle:
image: amir20/dozzle:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /path/to/certs:/certs
ports:
- 8080:8080
environment:
DOZZLE_REMOTE_HOST: tcp://167.99.1.1:2376,tcp://167.99.1.2:2376
When you get under the key remote socket output like this, its already enabled:
You need to make sure appropriate certs are provided in `/certs/167.99.1.1/{ca,cert,key}.pem` and `/certs/167.99.1.2/{ca,cert,key}.pem` for both hosts to work.
```
remoteSocket:
exists: true
path: /run/user/1000/podman/podman.sock
```
### Adding health check
If it's not enabled please follow [this tutorial](https://github.com/containers/podman/blob/main/docs/tutorials/socket_activation.md) to enable it.
Dozzle doesn't enable healthcheck by default as it adds extra CPU usage. `healthcheck` can be enabled manually.
Once you have the podman remote socket you can run Dozzle on podman.
version: "3"
services:
dozzle:
container_name: dozzle
image: amir20/dozzle:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
ports:
- 8080:8080
environment:
DOZZLE_LEVEL: trace
healthcheck:
test: [ "CMD", "/dozzle", "healthcheck" ]
interval: 3s
timeout: 30s
retries: 5
start_period: 30s
```
podman run --volume=/run/user/1000/podman/podman.sock:/var/run/docker.sock:ro -d -p 8888:8080 amir20/dozzle:latest
```
#### Security
## Security
You can control the device Dozzle binds to by passing `--addr` parameter. For example,
Dozzle supports file based authentication and forward proxy like [Authelia](https://www.authelia.com/). These are documented at https://dozzle.dev/guide/authentication.
$ docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8888:1224 amir20/dozzle:latest --addr localhost:1224
will bind to `localhost` on port `1224`. You can then use a reverse proxy to control who can see dozzle.
If you wish to restrict the containers shown you can pass the `--filter` parameter. For example,
$ docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8888:1224 amir20/dozzle:latest --filter name=foo
this would then only allow you to view containers with a name starting with "foo". You can use other filters like `status` as well, please check the official docker [command line docs](https://docs.docker.com/engine/reference/commandline/ps/#filtering) for available filters. Multiple `--filter` arguments can be provided.
#### Authentication
Dozzle supports a very simple authentication out of the box with just username and password. You should deploy using SSL to keep the credentials safe. See configuration to use `--username` and `--password`. You can also use [docker secrets](https://docs.docker.com/engine/swarm/secrets/) `--usernamefile` and `--passwordfile`.
#### Changing base URL
Dozzle by default mounts to "/". If you want to control the base path you can use the `--base` option. For example, if you want to mount at "/foobar",
then you can override by using `--base /foobar`. See env variables below for using `DOZZLE_BASE` to change this.
$ docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir20/dozzle:latest --base /foobar
Dozzle will be available at [http://localhost:8080/foobar/](http://localhost:8080/foobar/).
#### Analytics collected
## Analytics collected
Dozzle collects anonymous user configurations using Google Analytics. Why? Dozzle is an open source project with no funding. As a result, there is no time to do user studies of Dozzle. Analytics is collected to prioritize features and fixes based on how people use Dozzle. This data is completely public and can be viewed live using [ Data Studio dashboard](https://datastudio.google.com/s/naeIu0MiWsY).
If you do not want to be tracked at all, see the `--no-analytics` flag below.
#### Environment variables and configuration
## 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` |
| `--filter` | `DOZZLE_FILTER` | `""` |
| `--username` | `DOZZLE_USERNAME` | `""` |
| `--password` | `DOZZLE_PASSWORD` | `""` |
| `--usernamefile` | `DOZZLE_USERNAME_FILE` | `""` |
| `--passwordfile` | `DOZZLE_PASSWORD_FILE` | `""` |
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
| `--remote-host` | `DOZZLE_REMOTE_HOST` | |
| 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 |
| `--wait-for-docker-seconds` | `DOZZLE_WAIT_FOR_DOCKER_SECONDS` | 0 |
| `--filter` | `DOZZLE_FILTER` | `""` |
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
| `--remote-host` | `DOZZLE_REMOTE_HOST` | |
## Troubleshooting and FAQs
## Support
<details>
<summary>I installed Dozzle, but logs are slow or they never load. Help!</summary>
There are many ways you can support Dozzle:
Dozzle uses Server Sent Events (SSE) which connects to a server using a HTTP stream without closing the connection. If any proxy tries to buffer this connection, then Dozzle never receives the data and hangs forever waiting for the reverse proxy to flush the buffer. Since version `1.23.0`, Dozzle sends the `X-Accel-Buffering: no` header which should stop reverse proxies buffering. However, some proxies may ignore this header. In those cases, you need to explicitly disable any buffering.
- Use it! Write about it! Star it! If you love Dozzle, drop me a line and tell me what you love.
- Blog about Dozzle to spread the word. If you are good at writing send PRs to improve the documentation at [dozzle.dev](https://dozzle.dev/)
- Sponsor my work at https://www.buymeacoffee.com/amirraminfar
Below is an example with nginx and using `proxy_pass` to disable buffering.
```
server {
...
location / {
proxy_pass http://<dozzle.container.ip.address>:8080;
}
location /api {
proxy_pass http://<dozzle.container.ip.address>:8080;
proxy_buffering off;
proxy_cache off;
}
}
```
</details>
<details>
<summary>What data does Dozzle collect?</summary>
Dozzle does collect some analytics. Analytics is anonymous usage tracking of the features which are used the most. See the section above on how to disable any analytic collection.
In the browser, Dozzle has a [strict](https://github.com/amir20/dozzle/blob/master/web/csp.go#L9) Content Security Policy which only allows the following policies:
- Allow connect to `api.github.com` to fetch most recent version.
- Only allow `<script>` and `<style>` files from `self`
Dozzle opens all links with `rel="noopener"`.
</details>
<details>
<summary>We have tools that uses Dozzle when a new container is created. How can I get a direct link to a container by name?</summary>
Dozzle has a [special route](https://github.com/amir20/dozzle/blob/master/assets/pages/Show.vue) that can be used to search containers by name and then forward to that container. For example, if you have a container with name `"foo.bar"` and id `abc123`, you can send your users to `/show?name=foo.bar` which will be forwarded to `/container/abc123`.
</details>
<details>
<summary>I installed Dozzle but memory consumption doesn't show up!</summary>
_This is an issue specific to ARM devices_
Dozzle uses the Docker API to gather information about the containers' memory usage. If the memory usage is not showing up, then it is likely that the Docker API is not returning the memory usage.
You can verify this by running `docker info`, and you should see the following:
```
WARNING: No memory limit support
WARNING: No swap limit support
```
In this case, you'll need to add the following line to your `/boot/cmdline.txt` file and reboot your device.
```
cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1
```
</details>
<a href="https://www.buymeacoffee.com/amirraminfar" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
## License
@@ -232,6 +129,6 @@ To Build and test locally:
1. Install [NodeJs](https://nodejs.org/en/download/) and [pnpm](https://pnpm.io/installation).
2. Install [Go](https://go.dev/doc/install).
3. Install [reflex](https://github.com/cespare/reflex) with `go get -u github.com/cespare/reflex` outside of dozzle.
3. Install [reflex](https://github.com/cespare/reflex) with `go install github.com/cespare/reflex@latest`.
4. Install node modules `pnpm install`.
5. Do `pnpm dev`
+5
View File
@@ -0,0 +1,5 @@
# Security Policy
## Reporting a Vulnerability
Dozzle is checked daily with https://docs.docker.com/scout/ and its dependencies are updated regularly. If you find a security bug, please update to the latest. For vulnerability on latest images please file an issue on Github.
-18
View File
@@ -1,18 +0,0 @@
package analytics
type StartEvent struct {
ClientId string `json:"-"`
Version string `json:"version"`
FilterLength int `json:"filterLength"`
RemoteHostLength int `json:"remoteHostLength"`
CustomAddress bool `json:"customAddress"`
CustomBase bool `json:"customBase"`
Protected bool `json:"protected"`
HasHostname bool `json:"hasHostname"`
}
type RequestEvent struct {
ClientId string `json:"-"`
TotalContainers int `json:"totalContainers"`
RunningContainers int `json:"runningContainers"`
}
+30 -1
View File
@@ -22,5 +22,34 @@ watchEffect(() => {
}
});
</script>
<style lang="postcss">
html.has-custom-scrollbars {
::-webkit-scrollbar {
width: 8px;
display: content;
}
<style scoped lang="scss"></style>
::-webkit-scrollbar-thumb {
background-color: rgba(128, 128, 128, 0.33);
outline: 1px solid slategrey;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:active {
background-color: #777;
}
::-webkit-scrollbar-track {
background-color: transparent;
}
::-webkit-scrollbar-track:hover {
background-color: rgba(64, 64, 64, 0.33);
}
section main {
scrollbar-color: #353535 transparent;
scrollbar-width: thin;
}
}
</style>
+122 -71
View File
@@ -12,19 +12,22 @@ declare global {
const $ref: typeof import('vue/macros')['$ref']
const $shallowRef: typeof import('vue/macros')['$shallowRef']
const $toRef: typeof import('vue/macros')['$toRef']
const DEFAULT_SETTINGS: typeof import('./composables/settings')['DEFAULT_SETTINGS']
const DEFAULT_SETTINGS: typeof import('./stores/settings')['DEFAULT_SETTINGS']
const EffectScope: typeof import('vue')['EffectScope']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const arrayEquals: typeof import('./utils/index')['arrayEquals']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const collapseNav: typeof import('./composables/settings')['collapseNav']
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 containerContext: typeof import('./composable/containerContext')['containerContext']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: typeof import('vue')['createApp']
@@ -38,6 +41,7 @@ declare global {
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']
@@ -52,20 +56,22 @@ declare global {
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const getDeep: typeof import('./utils/index')['getDeep']
const globalShowPopup: typeof import('./composables/popup')['globalShowPopup']
const globalShowPopup: typeof import('./composable/popup')['globalShowPopup']
const h: typeof import('vue')['h']
const hourStyle: typeof import('./composables/settings')['hourStyle']
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('./composables/media')['isMobile']
const isMobile: typeof import('./composable/media')['isMobile']
const isObject: typeof import('./utils/index')['isObject']
const isPinnedContainer: typeof import('./composables/storage')['isPinnedContainer']
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('./composables/settings')['lightTheme']
const lightTheme: typeof import('./stores/settings')['lightTheme']
const locale: typeof import('./stores/settings')['locale']
const logSearchContext: typeof import('./composable/logSearchContext')['logSearchContext']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
@@ -73,7 +79,7 @@ declare global {
const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw']
const menuWidth: typeof import('./composables/settings')['menuWidth']
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']
@@ -95,9 +101,11 @@ declare global {
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const persistentVisibleKeys: typeof import('./composables/storage')['persistentVisibleKeys']
const pinnedContainers: typeof import('./composables/storage')['pinnedContainers']
const persistentVisibleKeys: typeof import('./composable/storage')['persistentVisibleKeys']
const pinnedContainers: typeof import('./composable/storage')['pinnedContainers']
const provide: typeof import('vue')['provide']
const provideContainerContext: typeof import('./composable/containerContext')['provideContainerContext']
const provideLocal: typeof import('@vueuse/core')['provideLocal']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
@@ -114,21 +122,21 @@ declare global {
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const search: typeof import('./composables/settings')['search']
const sessionHost: typeof import('./composables/storage')['sessionHost']
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('./composables/title')['setTitle']
const settings: typeof import('./composables/settings')['settings']
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('./composables/settings')['showAllContainers']
const showStd: typeof import('./composables/settings')['showStd']
const showTimestamp: typeof import('./composables/settings')['showTimestamp']
const size: typeof import('./composables/settings')['size']
const smallerScrollbars: typeof import('./composables/settings')['smallerScrollbars']
const softWrap: typeof import('./composables/settings')['softWrap']
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']
@@ -141,7 +149,6 @@ declare global {
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const togglePinnedContainer: typeof import('./composables/storage')['togglePinnedContainer']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
@@ -176,9 +183,12 @@ declare global {
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 useContainerContext: typeof import('./composable/containerContext')['useContainerContext']
const useContainerStore: typeof import('./stores/container')['useContainerStore']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
@@ -231,7 +241,8 @@ declare global {
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLink: typeof import('vue-router')['useLink']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useLogStream: typeof import('./composables/eventsource')['useLogStream']
const useLogSearchContext: typeof import('./composable/logSearchContext')['useLogSearchContext']
const useLogStream: typeof import('./composable/eventsource')['useLogStream']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
@@ -263,8 +274,10 @@ declare global {
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']
@@ -273,10 +286,11 @@ declare global {
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSearchFilter: typeof import('./composables/search')['useSearchFilter']
const useSearchFilter: typeof import('./composable/search')['useSearchFilter']
const useSeoMeta: typeof import('@vueuse/head')['useSeoMeta']
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']
@@ -302,6 +316,7 @@ declare global {
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']
@@ -310,7 +325,7 @@ declare global {
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('./composables/visible')['useVisibleFilter']
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']
@@ -335,15 +350,18 @@ declare global {
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, InjectionKey, PropType, Ref, VNode } from 'vue'
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
import('vue')
}
// for vue template auto import
import { UnwrapRef } from 'vue'
declare module 'vue' {
interface GlobalComponents {}
interface ComponentCustomProperties {
readonly $$: UnwrapRef<typeof import('vue/macros')['$$']>
readonly $: UnwrapRef<typeof import('vue/macros')['$']>
@@ -352,19 +370,22 @@ declare module 'vue' {
readonly $ref: UnwrapRef<typeof import('vue/macros')['$ref']>
readonly $shallowRef: UnwrapRef<typeof import('vue/macros')['$shallowRef']>
readonly $toRef: UnwrapRef<typeof import('vue/macros')['$toRef']>
readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./composables/settings')['DEFAULT_SETTINGS']>
readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./stores/settings')['DEFAULT_SETTINGS']>
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly collapseNav: UnwrapRef<typeof import('./composables/settings')['collapseNav']>
readonly automaticRedirect: UnwrapRef<typeof import('./stores/settings')['automaticRedirect']>
readonly collapseNav: UnwrapRef<typeof import('./stores/settings')['collapseNav']>
readonly compact: UnwrapRef<typeof import('./stores/settings')['compact']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
readonly config: UnwrapRef<typeof import('./stores/config')['default']>
readonly containerContext: UnwrapRef<typeof import('./composable/containerContext')['containerContext']>
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
@@ -378,6 +399,7 @@ declare module 'vue' {
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly dateLocale: UnwrapRef<typeof import('./stores/settings')['dateLocale']>
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
@@ -392,19 +414,21 @@ declare module 'vue' {
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getDeep: UnwrapRef<typeof import('./utils/index')['getDeep']>
readonly globalShowPopup: UnwrapRef<typeof import('./composables/popup')['globalShowPopup']>
readonly globalShowPopup: UnwrapRef<typeof import('./composable/popup')['globalShowPopup']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly hourStyle: UnwrapRef<typeof import('./composables/settings')['hourStyle']>
readonly hourStyle: UnwrapRef<typeof import('./stores/settings')['hourStyle']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isMobile: UnwrapRef<typeof import('./composables/media')['isMobile']>
readonly isMobile: UnwrapRef<typeof import('./composable/media')['isMobile']>
readonly isObject: UnwrapRef<typeof import('./utils/index')['isObject']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly lightTheme: UnwrapRef<typeof import('./composables/settings')['lightTheme']>
readonly lightTheme: UnwrapRef<typeof import('./stores/settings')['lightTheme']>
readonly locale: UnwrapRef<typeof import('./stores/settings')['locale']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
@@ -412,7 +436,7 @@ declare module 'vue' {
readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly menuWidth: UnwrapRef<typeof import('./composables/settings')['menuWidth']>
readonly menuWidth: UnwrapRef<typeof import('./stores/settings')['menuWidth']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
@@ -434,9 +458,11 @@ declare module 'vue' {
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly persistentVisibleKeys: UnwrapRef<typeof import('./composables/storage')['persistentVisibleKeys']>
readonly pinnedContainers: UnwrapRef<typeof import('./composables/storage')['pinnedContainers']>
readonly persistentVisibleKeys: UnwrapRef<typeof import('./composable/storage')['persistentVisibleKeys']>
readonly pinnedContainers: UnwrapRef<typeof import('./composable/storage')['pinnedContainers']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideContainerContext: UnwrapRef<typeof import('./composable/containerContext')['provideContainerContext']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
@@ -453,21 +479,21 @@ declare module 'vue' {
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly search: UnwrapRef<typeof import('./composables/settings')['search']>
readonly sessionHost: UnwrapRef<typeof import('./composables/storage')['sessionHost']>
readonly search: UnwrapRef<typeof import('./stores/settings')['search']>
readonly sessionHost: UnwrapRef<typeof import('./composable/storage')['sessionHost']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly setTitle: UnwrapRef<typeof import('./composables/title')['setTitle']>
readonly settings: UnwrapRef<typeof import('./composables/settings')['settings']>
readonly setTitle: UnwrapRef<typeof import('./composable/title')['setTitle']>
readonly settings: UnwrapRef<typeof import('./stores/settings')['settings']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly showAllContainers: UnwrapRef<typeof import('./composables/settings')['showAllContainers']>
readonly showStd: UnwrapRef<typeof import('./composables/settings')['showStd']>
readonly showTimestamp: UnwrapRef<typeof import('./composables/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./composables/settings')['size']>
readonly smallerScrollbars: UnwrapRef<typeof import('./composables/settings')['smallerScrollbars']>
readonly softWrap: UnwrapRef<typeof import('./composables/settings')['softWrap']>
readonly showAllContainers: UnwrapRef<typeof import('./stores/settings')['showAllContainers']>
readonly showStd: UnwrapRef<typeof import('./stores/settings')['showStd']>
readonly showTimestamp: UnwrapRef<typeof import('./stores/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./stores/settings')['size']>
readonly smallerScrollbars: UnwrapRef<typeof import('./stores/settings')['smallerScrollbars']>
readonly softWrap: UnwrapRef<typeof import('./stores/settings')['softWrap']>
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
readonly stripVersion: UnwrapRef<typeof import('./utils/index')['stripVersion']>
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
@@ -480,7 +506,6 @@ declare module 'vue' {
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly togglePinnedContainer: UnwrapRef<typeof import('./composables/storage')['togglePinnedContainer']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
@@ -515,9 +540,12 @@ declare module 'vue' {
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']>
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useContainerActions: UnwrapRef<typeof import('./composable/containerActions')['useContainerActions']>
readonly useContainerContext: UnwrapRef<typeof import('./composable/containerContext')['useContainerContext']>
readonly useContainerStore: UnwrapRef<typeof import('./stores/container')['useContainerStore']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
@@ -570,7 +598,8 @@ declare module 'vue' {
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useLogStream: UnwrapRef<typeof import('./composables/eventsource')['useLogStream']>
readonly useLogSearchContext: UnwrapRef<typeof import('./composable/logSearchContext')['useLogSearchContext']>
readonly useLogStream: UnwrapRef<typeof import('./composable/eventsource')['useLogStream']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
@@ -602,8 +631,10 @@ declare module 'vue' {
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
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']>
@@ -612,10 +643,11 @@ declare module 'vue' {
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSearchFilter: UnwrapRef<typeof import('./composables/search')['useSearchFilter']>
readonly useSearchFilter: UnwrapRef<typeof import('./composable/search')['useSearchFilter']>
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSimpleRefHistory: UnwrapRef<typeof import('./utils/index')['useSimpleRefHistory']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
@@ -641,6 +673,7 @@ declare module 'vue' {
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
readonly useToast: UnwrapRef<typeof import('./composable/toast')['useToast']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
@@ -649,7 +682,7 @@ declare module 'vue' {
readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
readonly useVisibleFilter: UnwrapRef<typeof import('./composables/visible')['useVisibleFilter']>
readonly useVisibleFilter: UnwrapRef<typeof import('./composable/visible')['useVisibleFilter']>
readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
@@ -674,9 +707,11 @@ declare module 'vue' {
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
readonly withBase: UnwrapRef<typeof import('./stores/config')['withBase']>
}
}
declare module '@vue/runtime-core' {
interface GlobalComponents {}
interface ComponentCustomProperties {
readonly $$: UnwrapRef<typeof import('vue/macros')['$$']>
readonly $: UnwrapRef<typeof import('vue/macros')['$']>
@@ -685,19 +720,22 @@ declare module '@vue/runtime-core' {
readonly $ref: UnwrapRef<typeof import('vue/macros')['$ref']>
readonly $shallowRef: UnwrapRef<typeof import('vue/macros')['$shallowRef']>
readonly $toRef: UnwrapRef<typeof import('vue/macros')['$toRef']>
readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./composables/settings')['DEFAULT_SETTINGS']>
readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./stores/settings')['DEFAULT_SETTINGS']>
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly collapseNav: UnwrapRef<typeof import('./composables/settings')['collapseNav']>
readonly automaticRedirect: UnwrapRef<typeof import('./stores/settings')['automaticRedirect']>
readonly collapseNav: UnwrapRef<typeof import('./stores/settings')['collapseNav']>
readonly compact: UnwrapRef<typeof import('./stores/settings')['compact']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
readonly config: UnwrapRef<typeof import('./stores/config')['default']>
readonly containerContext: UnwrapRef<typeof import('./composable/containerContext')['containerContext']>
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
@@ -711,6 +749,7 @@ declare module '@vue/runtime-core' {
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly dateLocale: UnwrapRef<typeof import('./stores/settings')['dateLocale']>
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
@@ -725,19 +764,21 @@ declare module '@vue/runtime-core' {
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getDeep: UnwrapRef<typeof import('./utils/index')['getDeep']>
readonly globalShowPopup: UnwrapRef<typeof import('./composables/popup')['globalShowPopup']>
readonly globalShowPopup: UnwrapRef<typeof import('./composable/popup')['globalShowPopup']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly hourStyle: UnwrapRef<typeof import('./composables/settings')['hourStyle']>
readonly hourStyle: UnwrapRef<typeof import('./stores/settings')['hourStyle']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isMobile: UnwrapRef<typeof import('./composables/media')['isMobile']>
readonly isMobile: UnwrapRef<typeof import('./composable/media')['isMobile']>
readonly isObject: UnwrapRef<typeof import('./utils/index')['isObject']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly lightTheme: UnwrapRef<typeof import('./composables/settings')['lightTheme']>
readonly lightTheme: UnwrapRef<typeof import('./stores/settings')['lightTheme']>
readonly locale: UnwrapRef<typeof import('./stores/settings')['locale']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
@@ -745,7 +786,7 @@ declare module '@vue/runtime-core' {
readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly menuWidth: UnwrapRef<typeof import('./composables/settings')['menuWidth']>
readonly menuWidth: UnwrapRef<typeof import('./stores/settings')['menuWidth']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
@@ -767,9 +808,11 @@ declare module '@vue/runtime-core' {
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly persistentVisibleKeys: UnwrapRef<typeof import('./composables/storage')['persistentVisibleKeys']>
readonly pinnedContainers: UnwrapRef<typeof import('./composables/storage')['pinnedContainers']>
readonly persistentVisibleKeys: UnwrapRef<typeof import('./composable/storage')['persistentVisibleKeys']>
readonly pinnedContainers: UnwrapRef<typeof import('./composable/storage')['pinnedContainers']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideContainerContext: UnwrapRef<typeof import('./composable/containerContext')['provideContainerContext']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
@@ -786,21 +829,21 @@ declare module '@vue/runtime-core' {
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly search: UnwrapRef<typeof import('./composables/settings')['search']>
readonly sessionHost: UnwrapRef<typeof import('./composables/storage')['sessionHost']>
readonly search: UnwrapRef<typeof import('./stores/settings')['search']>
readonly sessionHost: UnwrapRef<typeof import('./composable/storage')['sessionHost']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly setTitle: UnwrapRef<typeof import('./composables/title')['setTitle']>
readonly settings: UnwrapRef<typeof import('./composables/settings')['settings']>
readonly setTitle: UnwrapRef<typeof import('./composable/title')['setTitle']>
readonly settings: UnwrapRef<typeof import('./stores/settings')['settings']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly showAllContainers: UnwrapRef<typeof import('./composables/settings')['showAllContainers']>
readonly showStd: UnwrapRef<typeof import('./composables/settings')['showStd']>
readonly showTimestamp: UnwrapRef<typeof import('./composables/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./composables/settings')['size']>
readonly smallerScrollbars: UnwrapRef<typeof import('./composables/settings')['smallerScrollbars']>
readonly softWrap: UnwrapRef<typeof import('./composables/settings')['softWrap']>
readonly showAllContainers: UnwrapRef<typeof import('./stores/settings')['showAllContainers']>
readonly showStd: UnwrapRef<typeof import('./stores/settings')['showStd']>
readonly showTimestamp: UnwrapRef<typeof import('./stores/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./stores/settings')['size']>
readonly smallerScrollbars: UnwrapRef<typeof import('./stores/settings')['smallerScrollbars']>
readonly softWrap: UnwrapRef<typeof import('./stores/settings')['softWrap']>
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
readonly stripVersion: UnwrapRef<typeof import('./utils/index')['stripVersion']>
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
@@ -813,7 +856,6 @@ declare module '@vue/runtime-core' {
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly togglePinnedContainer: UnwrapRef<typeof import('./composables/storage')['togglePinnedContainer']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
@@ -848,9 +890,12 @@ declare module '@vue/runtime-core' {
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']>
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useContainerActions: UnwrapRef<typeof import('./composable/containerActions')['useContainerActions']>
readonly useContainerContext: UnwrapRef<typeof import('./composable/containerContext')['useContainerContext']>
readonly useContainerStore: UnwrapRef<typeof import('./stores/container')['useContainerStore']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
@@ -903,7 +948,8 @@ declare module '@vue/runtime-core' {
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useLogStream: UnwrapRef<typeof import('./composables/eventsource')['useLogStream']>
readonly useLogSearchContext: UnwrapRef<typeof import('./composable/logSearchContext')['useLogSearchContext']>
readonly useLogStream: UnwrapRef<typeof import('./composable/eventsource')['useLogStream']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
@@ -935,8 +981,10 @@ declare module '@vue/runtime-core' {
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
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']>
@@ -945,10 +993,11 @@ declare module '@vue/runtime-core' {
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSearchFilter: UnwrapRef<typeof import('./composables/search')['useSearchFilter']>
readonly useSearchFilter: UnwrapRef<typeof import('./composable/search')['useSearchFilter']>
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSimpleRefHistory: UnwrapRef<typeof import('./utils/index')['useSimpleRefHistory']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
@@ -974,6 +1023,7 @@ declare module '@vue/runtime-core' {
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
readonly useToast: UnwrapRef<typeof import('./composable/toast')['useToast']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
@@ -982,7 +1032,7 @@ declare module '@vue/runtime-core' {
readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
readonly useVisibleFilter: UnwrapRef<typeof import('./composables/visible')['useVisibleFilter']>
readonly useVisibleFilter: UnwrapRef<typeof import('./composable/visible')['useVisibleFilter']>
readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
@@ -1007,5 +1057,6 @@ declare module '@vue/runtime-core' {
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
readonly withBase: UnwrapRef<typeof import('./stores/config')['withBase']>
}
}
+30 -10
View File
@@ -10,16 +10,23 @@ declare module 'vue' {
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:macShift': typeof import('~icons/carbon/mac-shift')['default']
'Carbon:play': typeof import('~icons/carbon/play')['default']
'Carbon:restart': typeof import('~icons/carbon/restart')['default']
'Carbon:searchLocate': typeof import('~icons/carbon/search-locate')['default']
'Carbon:star': typeof import('~icons/carbon/star')['default']
'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']
'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:findInPage': typeof import('~icons/cil/find-in-page')['default']
'Cil:xCircle': typeof import('~icons/cil/x-circle')['default']
ComplexLogItem: typeof import('./components/LogViewer/ComplexLogItem.vue')['default']
ContainerHealth: typeof import('./components/LogViewer/ContainerHealth.vue')['default']
ContainerLogViewer: typeof import('./components/LogViewer/ContainerLogViewer.vue')['default']
ContainerPopup: typeof import('./components/LogViewer/ContainerPopup.vue')['default']
ContainerStat: typeof import('./components/LogViewer/ContainerStat.vue')['default']
ContainerTable: typeof import('./components/ContainerTable.vue')['default']
@@ -27,34 +34,45 @@ declare module 'vue' {
DateTime: typeof import('./components/common/DateTime.vue')['default']
DistanceTime: typeof import('./components/common/DistanceTime.vue')['default']
DockerEventLogItem: typeof import('./components/LogViewer/DockerEventLogItem.vue')['default']
DropdownMenu: typeof import('./components/DropdownMenu.vue')['default']
Dropdown: typeof import('./components/common/Dropdown.vue')['default']
DropdownMenu: typeof import('./components/common/DropdownMenu.vue')['default']
FieldList: typeof import('./components/LogViewer/FieldList.vue')['default']
FuzzySearchModal: typeof import('./components/FuzzySearchModal.vue')['default']
'Ic:sharpKeyboardReturn': typeof import('~icons/ic/sharp-keyboard-return')['default']
InfiniteLoader: typeof import('./components/InfiniteLoader.vue')['default']
KeyShortcut: typeof import('./components/KeyShortcut.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']
LogActionsToolbar: typeof import('./components/LogViewer/LogActionsToolbar.vue')['default']
LogContainer: typeof import('./components/LogViewer/LogContainer.vue')['default']
LogDate: typeof import('./components/LogViewer/LogDate.vue')['default']
LogEventSource: typeof import('./components/LogViewer/LogEventSource.vue')['default']
LogLevel: typeof import('./components/LogViewer/LogLevel.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']
LogViewerWithSource: typeof import('./components/LogViewer/LogViewerWithSource.vue')['default']
'Mdi:announcement': typeof import('~icons/mdi/announcement')['default']
'Mdi:arrowUp': typeof import('~icons/mdi/arrow-up')['default']
'Mdi:dotsVertical': typeof import('~icons/mdi/dots-vertical')['default']
'Mdi:lightChevronDoubleDown': typeof import('~icons/mdi-light/chevron-double-down')['default']
'Mdi:lightChevronLeft': typeof import('~icons/mdi-light/chevron-left')['default']
'Mdi:lightChevronRight': typeof import('~icons/mdi-light/chevron-right')['default']
'Mdi:lightCog': typeof import('~icons/mdi-light/cog')['default']
'Mdi:lightLogout': typeof import('~icons/mdi-light/logout')['default']
'Mdi:lightMagnify': typeof import('~icons/mdi-light/magnify')['default']
'Mdi:check': typeof import('~icons/mdi/check')['default']
'Mdi:chevronDoubleDown': typeof import('~icons/mdi/chevron-double-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:hamburgerMenu': typeof import('~icons/mdi/hamburger-menu')['default']
'Mdi:keyboardEsc': typeof import('~icons/mdi/keyboard-esc')['default']
'Mdi:magnify': typeof import('~icons/mdi/magnify')['default']
MobileMenu: typeof import('./components/common/MobileMenu.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']
PageWithLinks: typeof import('./components/PageWithLinks.vue')['default']
'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']
Popup: typeof import('./components/Popup.vue')['default']
Releases: typeof import('./components/Releases.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ScrollableView: typeof import('./components/ScrollableView.vue')['default']
@@ -67,6 +85,8 @@ declare module 'vue' {
StatMonitor: typeof import('./components/LogViewer/StatMonitor.vue')['default']
StatSparkline: typeof import('./components/LogViewer/StatSparkline.vue')['default']
Tag: typeof import('./components/common/Tag.vue')['default']
TimedButton: typeof import('./components/common/TimedButton.vue')['default']
Toggle: typeof import('./components/common/Toggle.vue')['default']
ZigZag: typeof import('./components/LogViewer/ZigZag.vue')['default']
}
}
+3 -14
View File
@@ -1,7 +1,7 @@
<template>
<div class="is-relative">
<div class="bar"></div>
<div class="is-overlay">
<div class="relative">
<div class="bar h-7 origin-left rounded-br rounded-tr bg-primary transition-transform"></div>
<div class="absolute inset-0 flex flex-col justify-center px-2 text-sm">
<slot></slot>
</div>
</div>
@@ -15,17 +15,6 @@ const minValue = computed(() => Math.min(value, 1));
<style scoped>
.bar {
height: 1.5em;
background-color: var(--primary-color);
transform-origin: left;
transform: scaleX(v-bind(minValue));
transition: transform 0.2s ease-out;
border-top-right-radius: 0.2em;
border-bottom-right-radius: 0.2em;
}
.is-overlay {
font-size: 0.9em;
padding: 0 0.5em;
}
</style>
+50 -30
View File
@@ -1,6 +1,15 @@
!
<template>
<table class="table is-fullwidth">
<div class="text-right">
Show per page
<dropdown-menu
class="dropdown-left btn-xs md:btn-sm"
v-model="perPage"
:options="pageSizes.map((i) => ({ label: i.toLocaleString(), value: i }))"
v-if="containers.length > 0"
/>
</div>
<table class="table table-lg bg-base">
<thead>
<tr :data-direction="direction > 0 ? 'asc' : 'desc'">
<th
@@ -10,12 +19,10 @@
:class="{ 'selected-sort': key === sortField }"
v-show="isVisible(key)"
>
<a>
<span class="icon-text">
<span>{{ $t(value.label) }}</span>
<span class="icon">
<mdi:arrow-up />
</span>
<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>
@@ -28,7 +35,7 @@
{{ container.name }}
</router-link>
</td>
<td v-if="isVisible('host')">{{ container.host }}</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>
@@ -46,15 +53,18 @@
</tr>
</tbody>
</table>
<nav class="pagination is-right" role="navigation" aria-label="pagination" v-if="isPaginated">
<ul class="pagination-list">
<li v-for="i in totalPages">
<a class="pagination-link" :class="{ 'is-current': i === currentPage }" @click.prevent="currentPage = i">{{
i
}}</a>
</li>
</ul>
</nav>
<div class="p-4 text-center">
<nav class="join" v-if="isPaginated">
<button
v-for="i in totalPages"
class="btn join-item"
:class="{ 'btn-primary': i === currentPage }"
@click="currentPage = i"
>
{{ i }}
</button>
</nav>
</div>
</template>
<script setup lang="ts">
@@ -69,7 +79,7 @@ const fields = {
},
host: {
label: "label.host",
sortFunc: (a: Container, b: Container) => a.host.localeCompare(b.host) * direction.value,
sortFunc: (a: Container, b: Container) => a.hostLabel.localeCompare(b.hostLabel) * direction.value,
mobileVisible: false,
},
state: {
@@ -94,12 +104,14 @@ const fields = {
},
};
const { containers, perPage = 15 } = defineProps<{
const { containers } = defineProps<{
containers: Container[];
perPage?: number;
}>();
type keys = keyof typeof fields;
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,
@@ -114,12 +126,12 @@ const sortedContainers = computedWithControl(
},
);
const totalPages = computed(() => Math.ceil(sortedContainers.value.length / perPage));
const totalPages = computed(() => Math.ceil(sortedContainers.value.length / perPage.value));
const isPaginated = computed(() => totalPages.value > 1);
const currentPage = ref(1);
const paginated = computed(() => {
const start = (currentPage.value - 1) * perPage;
const end = start + perPage;
const start = (currentPage.value - 1) * perPage.value;
const end = start + perPage.value;
return sortedContainers.value.slice(start, end);
});
@@ -137,19 +149,23 @@ function isVisible(field: keys) {
}
</script>
<style lang="scss" scoped>
.icon {
<style lang="postcss" scoped>
[data-icon] {
display: none;
transition: transform 0.2s ease-in-out;
[data-direction="desc"] & {
transform: rotate(180deg);
}
}
.selected-sort {
font-weight: bold;
border-color: var(--primary-color);
.icon {
display: inline-block;
th {
@apply border-b-2 border-base-lighter;
&.selected-sort {
font-weight: bold;
@apply border-primary;
[data-icon] {
display: inline-block;
}
}
}
@@ -159,4 +175,8 @@ tbody td {
text-overflow: ellipsis;
white-space: nowrap;
}
a {
@apply hover:text-primary;
}
</style>
-48
View File
@@ -1,48 +0,0 @@
<template>
<div class="dropdown is-hoverable">
<div class="dropdown-trigger">
<slot name="trigger">
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu">
<span class="icon">
<mdi:dots-vertical />
</span>
</button>
</slot>
</div>
<div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-content">
<slot></slot>
</div>
</div>
</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped>
.minimal .button {
background-color: rgba(0, 0, 0, 0);
border: none;
padding: 0.1em;
height: 100%;
& > .icon {
height: 100%;
& > svg {
align-self: flex-start;
height: 0.85em;
}
}
}
.is-top {
& .dropdown-menu {
top: 0;
}
&.is-last .dropdown-menu {
top: -30px;
}
}
</style>
@@ -0,0 +1,79 @@
import { createTestingPinia } from "@pinia/testing";
import { mount } from "@vue/test-utils";
import FuzzySearchModal from "./FuzzySearchModal.vue";
import { Container } from "@/models/Container";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { createI18n } from "vue-i18n";
import { useRouter } from "vue-router";
import { router } from "@/modules/router";
// @ts-ignore
import EventSource, { sources } from "eventsourcemock";
vi.mock("vue-router");
vi.mock("@/stores/config", () => ({
__esModule: true,
default: { base: "", hosts: [{ name: "localhost", id: "localhost" }] },
withBase: (path: string) => path,
}));
function createFuzzySearchModal() {
global.EventSource = EventSource;
const wrapper = mount(FuzzySearchModal, {
global: {
plugins: [
createI18n({}),
createTestingPinia({
createSpy: vi.fn,
initialState: {
container: {
containers: [
new Container("123", new Date(), "image", "test", "command", "host", {}, "status", "running", []),
new Container("345", new Date(), "image", "foo bar", "command", "host", {}, "status", "running", []),
new Container("567", new Date(), "image", "baz", "command", "host", {}, "status", "exited", []),
],
},
},
}),
],
},
});
return wrapper;
}
/**
* @vitest-environment jsdom
*/
describe("<FuzzySearchModal />", () => {
vi.mocked(useRouter).mockReturnValue({
...router,
push: vi.fn(),
});
beforeEach(() => {
vi.mocked(useRouter().push).mockReset();
});
test("shows all", async () => {
const wrapper = createFuzzySearchModal();
expect(wrapper.findAll("li").length).toBe(3);
});
test("search for foo", async () => {
const wrapper = createFuzzySearchModal();
await wrapper.find("input").setValue("foo");
expect(wrapper.findAll("li").length).toBe(1);
expect(wrapper.find("ul [data-name]").html()).toMatchInlineSnapshot(
`"<span data-v-dc2e8c61="" data-name=""><mark>foo</mark> bar</span>"`,
);
});
test("choose baz", async () => {
const wrapper = createFuzzySearchModal();
await wrapper.find("input").setValue("baz");
await wrapper.find("input").trigger("keyup.enter");
expect(useRouter().push).toHaveBeenCalledWith({ name: "container-id", params: { id: "567" } });
});
});
+94 -86
View File
@@ -1,143 +1,151 @@
<template>
<div class="panel">
<o-autocomplete
ref="autocomplete"
v-model="query"
:placeholder="$t('placeholder.search-containers')"
open-on-focus
keep-first
expanded
:data="data"
@select="selected"
>
<template #default="{ option: item }">
<div class="media">
<div class="media-left">
<span class="icon is-small" :class="item.state">
<octicon:container-24 />
</span>
<div class="dropdown dropdown-open w-full">
<div class="input input-primary flex h-auto items-center">
<mdi:magnify class="flex size-8" />
<input
tabindex="0"
class="input input-ghost input-lg flex-1 px-1"
ref="input"
@keyup.down="selectedIndex = Math.min(selectedIndex + 1, data.length - 1)"
@keyup.up="selectedIndex = Math.max(selectedIndex - 1, 0)"
@keyup.enter.exact="selected(data[selectedIndex].item)"
@keyup.alt.enter="addColumn(data[selectedIndex])"
v-model="query"
:placeholder="$t('placeholder.search-containers')"
/>
<mdi:keyboard-esc class="flex" />
</div>
<ul tabindex="0" class="menu dropdown-content !relative mt-2 w-full rounded-box bg-base-lighter p-2">
<li v-for="(result, index) in data">
<a
class="grid auto-cols-max grid-cols-[min-content,auto] gap-2 py-4"
@click.prevent="selected(result.item)"
@mouseenter="selectedIndex = index"
:class="index === selectedIndex ? 'focus' : ''"
>
<div :class="{ 'text-primary': result.item.state === 'running' }">
<octicon:container-24 />
</div>
<div class="media-content">{{ item.host }} / {{ item.name }}</div>
<div class="media-right">
<span
class="icon is-small column-icon"
@click.stop.prevent="addColumn(item)"
:title="$t('tooltip.pin-column')"
>
<cil:columns />
</span>
<div class="truncate">
<template v-if="config.hosts.length > 1">
<span class="font-light">{{ result.item.host }}</span> /
</template>
<span data-name v-html="matchedName(result)"></span>
</div>
</div>
</template>
</o-autocomplete>
<distance-time :date="result.item.created" class="text-xs font-light" />
<a
@click.stop.prevent="addColumn(result.item)"
:title="$t('tooltip.pin-column')"
class="hover:text-secondary"
>
<ic:sharp-keyboard-return v-if="index === selectedIndex" />
<cil:columns v-else />
</a>
</a>
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
import { useFuse } from "@vueuse/integrations/useFuse";
import { type FuseResultMatch } from "fuse.js";
const { maxResults: resultLimit = 20 } = defineProps<{
const { maxResults = 5 } = defineProps<{
maxResults?: number;
}>();
const close = defineEmit();
const query = ref("");
const autocomplete = ref<HTMLElement>();
const input = ref<HTMLInputElement>();
const selectedIndex = ref(0);
const router = useRouter();
const store = useContainerStore();
const { containers } = storeToRefs(store);
const list = computed(() => {
return containers.value.map(({ id, created, name, state, host }) => {
return containers.value.map(({ id, created, name, state, labels, hostLabel: host }) => {
return {
id,
created,
name,
state,
host,
labels: Object.entries(labels).map(([_, value]) => value),
};
});
});
const { results } = useFuse(query, list, {
fuseOptions: { keys: ["name"], includeScore: true },
resultLimit,
fuseOptions: {
keys: ["name", "host", "labels"],
includeScore: true,
useExtendedSearch: true,
threshold: 0.3,
includeMatches: true,
},
resultLimit: 10,
matchAllWhenSearchEmpty: true,
});
const data = computed(() => {
return results.value
.sort((a, b) => {
.toSorted((a, b) => {
if (a.score === b.score) {
if (a.item.state === "running" && b.item.state !== "running") {
if (a.item.state === b.item.state) {
return b.item.created - a.item.created;
} else if (a.item.state === "running" && b.item.state !== "running") {
return -1;
} else {
return 1;
}
} else if (a.score && b.score) {
return a.score - b.score;
} else {
return 0;
return a.score - b.score;
}
})
.map(({ item }) => item);
.slice(0, maxResults);
});
watchOnce(autocomplete, () => autocomplete.value?.focus());
watch(query, (data) => {
if (data.length > 0) {
selectedIndex.value = 0;
}
});
onMounted(() => input.value?.focus());
function selected({ id }: { id: string }) {
router.push({ name: "container-id", params: { id } });
close();
}
function addColumn(container: Container) {
function addColumn(container: { id: string }) {
store.appendActiveContainer(container);
close();
}
function matchedName({ item, matches = [] }: { item: { name: string }; matches?: FuseResultMatch[] }) {
const matched = matches.find((match) => match.key === "name");
if (matched) {
const { indices } = matched;
const result = [];
let lastIndex = 0;
for (const [start, end] of indices) {
result.push(item.name.slice(lastIndex, start));
result.push(`<mark>${item.name.slice(start, end + 1)}</mark>`);
lastIndex = end + 1;
}
result.push(item.name.slice(lastIndex));
return result.join("");
} else {
return item.name;
}
}
</script>
<style lang="scss" scoped>
.panel {
min-height: 400px;
width: 580px;
}
@media screen and (max-width: 768px) {
.panel {
min-height: 200px;
width: auto;
margin-left: 0.25rem !important;
margin-right: 0.25rem !important;
}
}
.running {
color: var(--primary-color);
}
.exited {
color: var(--scheme-main-ter);
}
.column-icon {
&:hover {
color: var(--secondary-color);
}
}
:deep(a.dropdown-item) {
padding-right: 1em;
.media-right {
visibility: hidden;
}
&:hover .media-right {
visibility: visible;
}
}
.icon {
vertical-align: middle;
<style scoped lang="postcss">
:deep(mark) {
@apply bg-transparent text-inherit underline underline-offset-2;
}
</style>
+4 -46
View File
@@ -1,16 +1,12 @@
<template>
<div ref="root" class="infinte-loader">
<div class="spinner" v-show="isLoading">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
<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 = () => {}, enabled } = defineProps<{
onLoadMore: () => void;
const { onLoadMore = () => Promise.resolve(), enabled } = defineProps<{
onLoadMore: () => Promise<void>;
enabled: boolean;
}>();
@@ -33,41 +29,3 @@ const observer = new IntersectionObserver(async (entries) => {
onMounted(() => observer.observe(root.value!));
onUnmounted(() => observer.disconnect());
</script>
<style scoped lang="scss">
.infinte-loader {
min-height: 1px;
}
.spinner {
margin: 10px auto 0;
width: 70px;
text-align: center;
& > div {
width: 12px;
height: 12px;
background-color: var(--primary-color);
border-radius: 100%;
display: inline-block;
animation: sk-bouncedelay 0.8s infinite ease-in-out both;
}
& .bounce1 {
animation-delay: -0.32s;
}
& .bounce2 {
animation-delay: -0.16s;
}
}
@keyframes sk-bouncedelay {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
</style>
-25
View File
@@ -1,25 +0,0 @@
<template>
<span class="icon-text">
<span class="icon mx-0" v-if="modifiers.includes('shift')">
<carbon:mac-shift />
</span>
<span class="icon mx-0" v-if="modifiers.includes('meta')">
<ph:command v-if="isMac" />
<ph:control-bold v-else />
</span>
<kbd class="is-uppercase is-family-sans-serif ml-1">{{ char }}</kbd>
</span>
</template>
<script lang="ts">
const isMac = /(Mac|iPhone|iPod|iPad)/i.test(navigator.userAgent);
</script>
<script lang="ts" setup>
const { modifiers = ["meta"], char } = defineProps<{ char: string; modifiers?: ("^" | "meta" | "shift")[] }>();
</script>
<style scoped lang="scss">
.icon {
width: unset;
}
</style>
+63
View File
@@ -0,0 +1,63 @@
<template>
<div class="flex items-center justify-end gap-4">
<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>
<router-link
:to="{ name: 'settings' }"
:aria-label="$t('title.settings')"
data-testid="settings"
class="btn btn-circle btn-sm"
>
<mdi:cog class="size-6" />
</router-link>
<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>
<template #content>
<div class="p-2">
<div class="font-bold">
{{ config.user.name }}
</div>
<div 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>
</li>
</ul>
</template>
</dropdown>
</div>
</template>
<script lang="ts" setup>
async function logout() {
await fetch(withBase("/api/token"), {
method: "DELETE",
});
location.reload();
}
const { hasUpdate, latest } = useReleases();
const latestTag = useProfileStorage("releaseSeen", config.version);
</script>
+22 -29
View File
@@ -1,28 +1,32 @@
<template>
<div class="columns is-1 is-variable is-mobile">
<div class="column is-narrow" v-if="showStd">
<div class="relative flex w-full gap-x-2">
<div v-if="showStd">
<log-std :std="logEntry.std"></log-std>
</div>
<div class="column is-narrow" v-if="showTimestamp">
<div v-if="showTimestamp">
<log-date :date="logEntry.date"></log-date>
</div>
<div class="column is-narrow is-flex">
<div class="flex">
<log-level :level="logEntry.level"></log-level>
</div>
<div class="column">
<ul class="fields" :class="{ expanded }" @click="expandToggle()">
<li v-for="(value, name) in validValues">
<span class="has-text-grey">{{ name }}=</span
><span class="has-text-weight-bold" v-if="value === null">&lt;null&gt;</span>
<div>
<ul class="fields cursor-pointer space-x-4" :class="{ expanded }" @click="expandToggle()">
<li v-for="(value, name) in validValues" class="inline-block">
<span class="text-light">{{ name }}=</span><span class="font-bold" v-if="value === null">&lt;null&gt;</span>
<template v-else-if="Array.isArray(value)">
<span class="has-text-weight-bold" v-html="markSearch(JSON.stringify(value))"> </span>
<span class="font-bold" v-html="markSearch(JSON.stringify(value))"> </span>
</template>
<span class="has-text-weight-bold" v-html="markSearch(value)" v-else></span>
<span class="font-bold" v-html="markSearch(value)" v-else></span>
</li>
<li class="has-text-grey" v-if="Object.keys(validValues).length === 0">all values are hidden</li>
<li class="text-light" v-if="Object.keys(validValues).length === 0">all values are hidden</li>
</ul>
<field-list :fields="logEntry.unfilteredMessage" :expanded="expanded" :visible-keys="visibleKeys"></field-list>
</div>
<log-message-actions
class="duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100"
:message="JSON.stringify(logEntry.message)"
:log-entry="logEntry"
/>
</div>
</template>
<script lang="ts" setup>
@@ -42,19 +46,16 @@ const validValues = computed(() => {
});
</script>
<style lang="scss" scoped>
<style lang="postcss" scoped>
.text-light {
@apply text-base-content/70;
}
.fields {
display: inline-block;
list-style: none;
&:hover {
cursor: pointer;
&::after {
content: "expand json";
color: var(--secondary-color);
display: inline-block;
margin-left: 0.5em;
@apply ml-2 inline-block text-secondary;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
}
@@ -64,13 +65,5 @@ const validValues = computed(() => {
content: "collapse json";
}
}
li {
display: inline-block;
& + li {
margin-left: 1em;
}
}
}
</style>
@@ -1,5 +1,5 @@
<template>
<div class="icon is-small health" :health="health" v-if="health" :title="health">
<div class="inline-flex size-4" :health="health" v-if="health" :title="health">
<cil:check-circle v-if="health == 'healthy'" />
<cil:x-circle v-else-if="health == 'unhealthy'" />
<cil:circle v-else />
@@ -14,14 +14,12 @@ defineProps<{
}>();
</script>
<style lang="scss" scoped>
.health {
&[health="unhealthy"] {
color: var(--red-color);
}
<style lang="postcss" scoped>
[health="unhealthy"] {
@apply text-red;
}
&[health="healthy"] {
color: var(--green-color);
}
[health="healthy"] {
@apply text-green;
}
</style>
@@ -0,0 +1,39 @@
<template>
<log-viewer :messages="filtered" :last-selected-item="lastSelectedItem" :visible-keys="visibleKeys" />
</template>
<script lang="ts" setup>
import { useRouteHash } from "@vueuse/router";
import { type JSONObject, LogEntry } from "@/models/LogEntry";
const props = defineProps<{
messages: LogEntry<string | JSONObject>[];
}>();
const { container } = useContainerContext();
const visibleKeys = persistentVisibleKeys(container);
const { filteredPayload } = useVisibleFilter(visibleKeys);
const { filteredMessages } = useSearchFilter();
const { messages } = toRefs(props);
const visible = filteredPayload(messages);
const filtered = filteredMessages(visible);
const { lastSelectedItem } = useLogSearchContext() as {
lastSelectedItem: Ref<LogEntry<string | JSONObject> | undefined>;
};
const routeHash = useRouteHash();
watch(
routeHash,
(hash) => {
if (hash) {
document.querySelector(`[data-key="${hash.substring(1)}"]`)?.scrollIntoView({ block: "center" });
}
},
{ immediate: true, flush: "post" },
);
</script>
<style scoped lang="postcss"></style>
@@ -1,17 +1,17 @@
<template>
<div>
<span class="has-text-weight-light"> RUNNING </span>
<span class="has-text-weight-semibold">
<span class="font-light capitalize"> RUNNING </span>
<span class="font-semibold">
<distance-time :date="container.created" strict :suffix="false"></distance-time>
</span>
</div>
<div>
<span class="has-text-weight-light"> LOAD </span>
<span class="has-text-weight-semibold"> {{ container.stat.cpu }}% </span>
<span class="font-light capitalize"> Load </span>
<span class="font-semibold"> {{ container.stat.cpu.toFixed(2) }}% </span>
</div>
<div>
<span class="has-text-weight-light"> MEM </span>
<span class="has-text-weight-semibold"> {{ formatBytes(container.stat.memoryUsage) }} </span>
<span class="font-light capitalize"> MEM </span>
<span class="font-semibold"> {{ formatBytes(container.stat.memoryUsage) }} </span>
</div>
</template>
@@ -22,5 +22,3 @@ const { container } = defineProps<{
container: Container;
}>();
</script>
<style lang="scss" scoped></style>
+10 -44
View File
@@ -1,34 +1,21 @@
<template>
<div class="is-size-7 is-uppercase columns is-marginless is-mobile is-vcentered" v-if="container.stat">
<stat-monitor
class="column is-narrow"
:data="memoryData"
label="mem"
:stat-value="formatBytes(unref(container.stat).memoryUsage)"
></stat-monitor>
<stat-monitor
class="column is-narrow"
:data="cpuData"
label="load"
:stat-value="unref(container.stat).cpu + '%'"
></stat-monitor>
<div class="flex gap-4" v-if="container.stat">
<stat-monitor :data="memoryData" label="mem" :stat-value="formatBytes(unref(container.stat).memoryUsage)" />
<stat-monitor :data="cpuData" label="load" :stat-value="Math.max(0, unref(container.stat).cpu).toFixed(2) + '%'" />
</div>
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
import { type ComputedRef } from "vue";
const container = inject("container") as ComputedRef<Container>;
const { container } = useContainerContext();
const cpuData = computedWithControl(
() => container.value.stat,
() => {
const history = container.value.statHistory;
const history = container.value.statsHistory;
const points: Point<unknown>[] = history.map((stat, i) => ({
x: i,
y: stat.snapshot.cpu,
value: stat.snapshot.cpu + "%",
y: Math.max(0, stat.cpu),
value: Math.max(0, stat.cpu).toFixed(2) + "%",
}));
return points;
},
@@ -37,34 +24,13 @@ const cpuData = computedWithControl(
const memoryData = computedWithControl(
() => container.value.stat,
() => {
const history = container.value.statHistory;
const history = container.value.statsHistory;
const points: Point<string>[] = history.map((stat, i) => ({
x: i,
y: stat.snapshot.memory,
value: formatBytes(stat.snapshot.memoryUsage),
y: stat.memory,
value: formatBytes(stat.memoryUsage),
}));
return points;
},
);
</script>
<style lang="scss" scoped>
.has-border {
border: 1px solid var(--primary-color);
border-radius: 3px;
padding: 1px 1px 0 1px;
display: flex;
overflow: hidden;
padding-top: 0.25em;
}
.has-background-body-color {
background-color: var(--body-background-color);
}
.is-top-left {
position: absolute;
top: 0;
left: 0.75em;
}
</style>
+31 -40
View File
@@ -1,48 +1,39 @@
<template>
<div class="columns is-marginless has-text-weight-bold is-family-monospace">
<div class="column is-ellipsis">
<container-health :health="container.health" v-if="container.health"></container-health>
<div class="name">
<span>{{ container.name }}</span
><span v-if="container.isSwarm" class="swarm-id">{{ container.swarmId }}</span>
<div class="flex flex-1 gap-1.5 truncate @container 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>
<tag class="is-hidden-mobile">{{ container.image.replace(/@sha.*/, "") }}</tag>
<span class="icon is-clickable" @click="togglePinnedContainer(container.storageKey)">
<carbon:star-filled v-if="pinned" />
<carbon:star v-else />
</span>
</div>
<container-health :health="container.health" v-if="container.health"></container-health>
<tag class="mobile-hidden hidden font-mono @3xl:block" size="small">
{{ container.image.replace(/@sha.*/, "") }}
</tag>
</div>
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
import { type ComputedRef } from "vue";
const container = inject("container") as ComputedRef<Container>;
const pinned = computed(() => pinnedContainers.value.has(container.value.storageKey));
</script>
<style lang="scss" scoped>
.icon {
vertical-align: middle;
}
.name {
display: inline-flex;
.swarm-id {
max-width: 0;
display: inline-block;
overflow: hidden;
white-space: nowrap;
transition: max-width 0.2s ease-in-out;
will-change: max-width;
}
&:hover {
.swarm-id {
max-width: 400px;
const { container } = useContainerContext();
const pinned = computed({
get: () => pinnedContainers.value.has(container.value.name),
set: (value) => {
if (value) {
pinnedContainers.value.add(container.value.name);
} else {
pinnedContainers.value.delete(container.value.name);
}
}
}
</style>
},
});
</script>
@@ -1,24 +1,73 @@
<template>
<span class="text" :data-event="logEntry.event" v-html="logEntry.message"></span>
<div class="flex-1 font-sans text-[0.9rem]">
<span class="whitespace-pre-wrap" :data-event="logEntry.event" v-html="logEntry.message"></span>
<div
class="alert alert-info mt-8 w-auto text-[1rem] md:mx-auto md:w-1/2"
v-if="nextContainer && logEntry.event === 'container-stopped'"
>
<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()">Cancel</TimedButton>
<router-link
:to="{ name: 'container-id', params: { id: nextContainer.id } }"
class="btn btn-primary btn-sm"
v-else
>
Redirect
</router-link>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { DockerEventLogEntry } from "@/models/LogEntry";
const router = useRouter();
const { showToast } = useToast();
const { t } = useI18n();
defineProps<{
const { logEntry } = defineProps<{
logEntry: DockerEventLogEntry;
}>();
const store = useContainerStore();
const { containers } = storeToRefs(store);
const { container } = useContainerContext();
const nextContainer = computed(
() =>
containers.value
.filter(
(c) =>
c.host === container.value.host &&
c.created > logEntry.date &&
c.name === container.value.name &&
c.state === "running",
)
.toSorted((a, b) => +a.created - +b.created)[0],
);
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="scss" scoped>
span {
&[data-event="container-stopped"] {
color: #f14668;
}
&[data-event="container-started"] {
color: hsl(141, 53%, 53%);
}
&.text {
white-space: pre-wrap;
}
<style lang="postcss" scoped>
[data-event="container-stopped"] {
@apply text-red;
}
[data-event="container-started"] {
@apply text-green;
}
</style>
+14 -10
View File
@@ -1,8 +1,8 @@
<template>
<ul v-if="expanded" ref="root">
<ul v-if="expanded" ref="root" class="ml-8">
<li v-for="(value, name) in fields">
<template v-if="isObject(value)">
<span class="has-text-grey">{{ name }}=</span>
<span class="text-light">{{ name }}=</span>
<field-list
:fields="value"
:parent-key="parentKey.concat(name)"
@@ -11,16 +11,20 @@
></field-list>
</template>
<template v-else-if="Array.isArray(value)">
<a @click="toggleField(name)"> {{ hasField(name) ? "remove" : "add" }}&nbsp;</a>
<span class="has-text-grey">{{ name }}=</span>[
<span class="has-text-weight-bold" v-for="(item, index) in value">
<a @click="toggleField(name)" class="link-primary cursor-pointer">
{{ hasField(name) ? "remove" : "add" }}&nbsp;</a
>
<span class="text-light">{{ name }}=</span>[
<span class="font-bold" v-for="(item, index) in value">
<span v-html="JSON.stringify(item)"></span><span v-if="index !== value.length - 1">,</span>
</span>
]
</template>
<template v-else>
<a @click="toggleField(name)"> {{ hasField(name) ? "remove" : "add" }}&nbsp;</a>
<span class="has-text-grey">{{ name }}=</span><span class="has-text-weight-bold" v-html="value"></span>
<a @click="toggleField(name)" class="link-primary cursor-pointer">
{{ hasField(name) ? "remove" : "add" }}&nbsp;</a
>
<span class="text-light">{{ name }}=</span><span class="font-bold" v-html="value"></span>
</template>
</li>
</ul>
@@ -68,8 +72,8 @@ function fieldIndex(field: string) {
}
</script>
<style lang="scss" scoped>
ul {
margin-left: 2em;
<style scoped lang="postcss">
.text-light {
@apply text-base-content/70;
}
</style>
+108 -116
View File
@@ -1,137 +1,129 @@
<template>
<dropdown-menu class="is-right">
<template #trigger>
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu">
<span class="icon">
<carbon:circle-solid class="is-red is-small" v-if="streamConfig.stderr" />
<carbon:circle-solid class="is-blue is-small" v-if="streamConfig.stdout" />
</span>
</button>
</template>
<a class="dropdown-item" @click="clear()">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<octicon:trash-24 />
</div>
</div>
<div class="level-right is-justify-content-space-between is-flex-grow-1">
<div class="level-item">{{ $t("toolbar.clear") }}</div>
<div class="level-item"><key-shortcut char="k" :modifiers="['shift', 'meta']"></key-shortcut></div>
</div>
</div>
</a>
<a class="dropdown-item" :href="`${base}/api/logs/download/${container.host}/${container.id}`">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<octicon:download-24 />
</div>
</div>
<div class="level-right">
<div class="level-item">{{ $t("toolbar.download") }}</div>
</div>
</div>
</a>
<hr class="dropdown-divider" />
<a class="dropdown-item" @click="showSearch = true">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<mdi:light-magnify />
</div>
</div>
<div class="level-right is-justify-content-space-between is-flex-grow-1">
<div class="level-item">{{ $t("toolbar.search") }}</div>
<div class="level-item"><key-shortcut char="f"></key-shortcut></div>
</div>
</div>
</a>
<hr class="dropdown-divider" />
<a
class="dropdown-item"
@click="
streamConfig.stdout = true;
streamConfig.stderr = true;
"
>
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<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" />
</label>
<ul tabindex="0" class="menu dropdown-content z-50 w-52 rounded-box bg-base p-1 shadow">
<li>
<a @click.prevent="clear()">
<octicon:trash-24 /> {{ $t("toolbar.clear") }}
<key-shortcut char="k" :modifiers="['shift', 'meta']"></key-shortcut>
</a>
</li>
<li>
<a :href="`${base}/api/logs/download/${container.host}/${container.id}`" download>
<octicon:download-24 /> {{ $t("toolbar.download") }}
</a>
</li>
<li>
<a @click.prevent="showSearch = true">
<mdi:magnify /> {{ $t("toolbar.search") }}
<key-shortcut char="f"></key-shortcut>
</a>
</li>
<li class="line"></li>
<li>
<a
@click="
streamConfig.stdout = true;
streamConfig.stderr = true;
"
>
<div class="flex size-4 gap-0.5">
<template v-if="streamConfig.stderr && streamConfig.stdout">
<carbon:circle-solid class="is-red is-small" />
<carbon:circle-solid class="is-blue is-small" />
<carbon:circle-solid class="w-2 text-red" />
<carbon:circle-solid class="w-2 text-blue" />
</template>
</div>
</div>
<div class="level-right">
{{ $t("toolbar.show-all") }}
</div>
</div>
</a>
<a
class="dropdown-item"
@click="
streamConfig.stdout = true;
streamConfig.stderr = false;
"
>
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<carbon:circle-solid class="is-blue is-small" v-if="!streamConfig.stderr && streamConfig.stdout" />
</a>
</li>
<li>
<a
@click="
streamConfig.stdout = true;
streamConfig.stderr = false;
"
>
<div class="flex size-4 flex-col gap-1">
<carbon:circle-solid class="w-2 text-blue" v-if="!streamConfig.stderr && streamConfig.stdout" />
</div>
</div>
<div class="level-right">
{{ $t("toolbar.show", { std: "STDOUT" }) }}
</div>
</div>
</a>
<a
class="dropdown-item"
@click="
streamConfig.stdout = false;
streamConfig.stderr = true;
"
>
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<carbon:circle-solid class="is-red is-small" v-if="streamConfig.stderr && !streamConfig.stdout" />
</a>
</li>
<li>
<a
@click="
streamConfig.stdout = false;
streamConfig.stderr = true;
"
>
<div class="flex size-4 flex-col gap-1">
<carbon:circle-solid class="w-2 text-red" v-if="streamConfig.stderr && !streamConfig.stdout" />
</div>
</div>
<div class="level-right">
<div class="level-item">
{{ $t("toolbar.show", { std: "STDERR" }) }}
</div>
</div>
</div>
</a>
</dropdown-menu>
{{ $t("toolbar.show", { std: "STDERR" }) }}
</a>
</li>
<!-- Container Actions (Enabled via config) -->
<template v-if="enableActions">
<li class="line"></li>
<li>
<button
@click="stop()"
:disabled="actionStates.stop || actionStates.restart"
v-if="container.state == 'running'"
>
<carbon:stop-filled-alt /> {{ $t("toolbar.stop") }}
</button>
<button
@click="start()"
:disabled="actionStates.start || actionStates.restart"
v-if="container.state != 'running'"
>
<carbon:play /> {{ $t("toolbar.start") }}
</button>
</li>
<li>
<button @click="restart()" :disabled="disableRestart">
<carbon:restart
:class="{
'animate-spin': actionStates.restart,
'text-secondary': actionStates.restart,
}"
/>
{{ $t("toolbar.restart") }}
</button>
</li>
</template>
</ul>
</div>
</template>
<script lang="ts" setup>
import { type ComputedRef } from "vue";
import { Container } from "@/models/Container";
const { showSearch } = useSearchFilter();
const { base } = config;
const { base, enableActions } = config;
const clear = defineEmit();
const container = inject("container") as ComputedRef<Container>;
const streamConfig = inject("stream-config") as { stdout: boolean; stderr: boolean };
const { container, streamConfig } = useContainerContext();
// container context is provided in the parent component: <LogContainer>
const { actionStates, start, stop, restart } = useContainerActions();
const disableRestart = computed(() => {
return actionStates.stop || actionStates.start || actionStates.restart;
});
</script>
<style lang="scss" scoped>
.level-left .level-item {
width: 2.2em;
align-items: center;
margin-right: 0.5em;
<style scoped lang="postcss">
li.line {
@apply h-px bg-base-content/20;
}
.is-small {
width: 0.6em;
a {
@apply whitespace-nowrap;
}
</style>
+8 -33
View File
@@ -1,20 +1,14 @@
<template>
<scrollable-view :scrollable="scrollable" v-if="container">
<template #header v-if="showTitle">
<div class="mr-0 columns is-mobile is-vcentered is-marginless has-boxshadow">
<div class="column is-clipped is-paddingless">
<container-title @close="$emit('close')" />
</div>
<div class="column is-narrow is-paddingless">
<container-stat />
</div>
<div class="mx-2 flex items-center gap-2 md:ml-4">
<container-title @close="$emit('close')" />
<container-stat class="ml-auto" />
<div class="mr-2 column is-narrow is-paddingless is-hidden-mobile">
<log-actions-toolbar @clear="onClearClicked()" />
</div>
<div class="mr-2 column is-narrow is-paddingless" v-if="closable">
<button class="delete is-medium" @click="close()"></button>
</div>
<log-actions-toolbar @clear="onClearClicked()" class="mobile-hidden" />
<a class="btn btn-circle btn-xs" @click="close()" v-if="closable">
<mdi:close />
</a>
</div>
</template>
<template #default="{ setLoading }">
@@ -41,12 +35,8 @@ const {
const close = defineEmit();
const store = useContainerStore();
const container = store.currentContainer($$(id));
const config = reactive({ stdout: true, stderr: true });
provide("container", container);
provide("stream-config", config);
provideContainerContext(container);
const viewer = ref<InstanceType<typeof LogViewerWithSource>>();
@@ -61,18 +51,3 @@ onKeyStroke("k", (e) => {
}
});
</script>
<style lang="scss" scoped>
button.delete {
background-color: var(--scheme-main-ter);
opacity: 0.6;
&:after,
&:before {
background-color: var(--text-color);
}
&:hover {
opacity: 1;
}
}
</style>
+3 -12
View File
@@ -1,19 +1,10 @@
<template>
<date-time :date="date" class="date"></date-time>
<tag size="small">
<date-time :date="date" class="whitespace-nowrap text-blue"></date-time>
</tag>
</template>
<script lang="ts" setup>
defineProps<{
date: Date;
}>();
</script>
<style lang="scss" scoped>
.date {
padding-left: 5px;
padding-right: 5px;
border-radius: 3px;
white-space: nowrap;
background-color: var(--scheme-main-ter);
color: #258ccd;
}
</style>
@@ -1,18 +1,21 @@
import { mount } from "@vue/test-utils";
import { createTestingPinia } from "@pinia/testing";
import { mount } from "@vue/test-utils";
import { containerContext } from "@/composable/containerContext";
import { useSearchFilter } from "@/composable/search";
import { settings } from "@/stores/settings";
// @ts-ignore
import EventSource, { sources } from "eventsourcemock";
import LogEventSource from "./LogEventSource.vue";
import LogViewer from "./LogViewer.vue";
import { settings } from "../../composables/settings";
import { useSearchFilter } from "@/composables/search";
import { vi, describe, expect, beforeEach, test, afterEach } from "vitest";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { computed, nextTick } from "vue";
import { createI18n } from "vue-i18n";
import { createRouter, createWebHistory } from "vue-router";
import LogEventSource from "./LogEventSource.vue";
import ContainerLogViewer from "./ContainerLogViewer.vue";
vi.mock("@/stores/config", () => ({
__esModule: true,
default: { base: "", hosts: ["localhost"] },
default: { base: "", hosts: [{ name: "localhost", id: "localhost" }] },
withBase: (path: string) => path,
}));
/**
@@ -66,26 +69,28 @@ describe("<LogEventSource />", () => {
return mount(LogEventSource, {
global: {
plugins: [router, createTestingPinia({ createSpy: vi.fn })],
plugins: [router, createTestingPinia({ createSpy: vi.fn }), createI18n({})],
components: {
LogViewer,
ContainerLogViewer,
},
provide: {
container: computed(() => ({ id: "abc", image: "test:v123", host: "localhost" })),
"stream-config": reactive({ stdout: true, stderr: true }),
[containerContext as symbol]: {
container: computed(() => ({ id: "abc", image: "test:v123", host: "localhost" })),
streamConfig: reactive({ stdout: true, stderr: true }),
},
scrollingPaused: computed(() => false),
},
},
slots: {
default: `
<template #scoped="params"><log-viewer :messages="params.messages"></log-viewer></template>
<template #scoped="params"><container-log-viewer :messages="params.messages" /></template>
`,
},
props: {},
});
}
const sourceUrl = "/api/logs/stream/localhost/abc?lastEventId=&stdout=1&stderr=1";
const sourceUrl = "/api/logs/stream/localhost/abc?stdout=1&stderr=1";
test("renders correctly", async () => {
const wrapper = createLogEventSource();
@@ -135,19 +140,6 @@ describe("<LogEventSource />", () => {
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
});
test("should render messages with color", async () => {
const wrapper = createLogEventSource();
sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({
data: '{"ts":1560336942459,"m":"\\u001b[30mblack\\u001b[37mwhite", "id":1}',
});
vi.runAllTimers();
await nextTick();
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
});
test("should render messages with html entities", async () => {
const wrapper = createLogEventSource();
sources[sourceUrl].emitOpen();
@@ -4,14 +4,9 @@
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
import { type ComputedRef } from "vue";
const loadingMore = defineEmit<[value: boolean]>();
const container = inject("container") as ComputedRef<Container>;
const config = inject("stream-config") as { stdout: boolean; stderr: boolean };
const { messages, loadOlderLogs } = useLogStream(container, config);
const { messages, loadOlderLogs } = useLogStream();
const beforeLoading = () => loadingMore(true);
const afterLoading = () => loadingMore(false);
+35 -44
View File
@@ -1,5 +1,5 @@
<template>
<div :class="level" :data-position="position"></div>
<div :data-level="level" :data-position="position" class="mt-1.5 size-2.5 flex-none rounded-lg"></div>
</template>
<script lang="ts" setup>
import { Position } from "@/models/LogEntry";
@@ -10,54 +10,45 @@ defineProps<{
}>();
</script>
<style lang="scss" scoped>
div {
display: inline-block;
width: 0.7em;
height: 0.7em;
border-radius: 0.5em;
align-self: auto;
margin-top: 0.4em;
<style lang="postcss" scoped>
[data-position="start"] {
border-radius: 0.5em 0.5em 0 0;
height: 70%;
margin-bottom: -0.4em;
margin-top: auto;
align-self: flex-end;
}
&[data-position="start"] {
border-radius: 0.5em 0.5em 0 0;
height: 70%;
margin-bottom: -0.2em;
margin-top: auto;
align-self: flex-end;
}
[data-position="middle"] {
border-radius: 0;
height: auto;
margin: -0.4em 0;
align-self: stretch;
}
&[data-position="middle"] {
border-radius: 0;
height: auto;
margin: -0.2em 0;
align-self: auto;
}
[data-position="end"] {
border-radius: 0 0 0.5em 0.5em;
height: 70%;
margin-top: -0.4em;
align-self: flex-start;
}
&[data-position="end"] {
border-radius: 0 0 0.5em 0.5em;
height: 70%;
margin-top: -0.2em;
align-self: flex-start;
}
[data-level="debug"],
[data-level="trace"] {
@apply bg-purple;
}
&.debug,
&.trace {
background-color: var(--purple-color);
}
[data-level="info"] {
@apply bg-green;
}
&.info {
background-color: var(--green-color);
}
[data-level="error"],
[data-level="fatal"] {
@apply bg-red;
}
&.error,
&.fatal {
background-color: var(--red-color);
}
&.warn,
&.warning {
background-color: var(--orange-color);
}
[data-level="warn"],
[data-level="warning"] {
@apply bg-orange;
}
</style>
@@ -0,0 +1,60 @@
<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 && message.trim() != ''"
: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="copyLogMessageToClipBoard()"
>
<carbon:copy-file />
</span>
</div>
<div
class="flex min-w-[0.98rem] items-start justify-end align-bottom hover:cursor-pointer"
:title="t('log_actions.jump_to_context')"
v-if="isSearching()"
>
<a
class="rounded bg-slate-800/60 px-1.5 py-1 text-primary hover:bg-slate-700"
@click="handleJumpLineSelected($event, logEntry)"
:href="`#${logEntry.id}`"
>
<carbon:search-locate />
</a>
</div>
</div>
</template>
<script lang="ts" setup>
import { LogEntry, JSONObject } from "@/models/LogEntry";
const { message, logEntry } = defineProps<{
message: string;
logEntry: LogEntry<string | JSONObject>;
}>();
const { showToast } = useToast();
const { copy, isSupported, copied } = useClipboard();
const { t } = useI18n();
const { isSearching } = useSearchFilter();
const { handleJumpLineSelected } = useLogSearchContext();
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>
+6 -8
View File
@@ -12,14 +12,12 @@ defineProps<{
}>();
</script>
<style lang="scss" scoped>
.tag {
&[std="stdout"] {
color: var(--blue-color);
}
<style lang="postcss" scoped>
[std="stdout"] {
@apply text-blue;
}
&[std="stderr"] {
color: var(--red-color);
}
[std="stderr"] {
@apply text-red;
}
</style>
+24 -75
View File
@@ -1,73 +1,30 @@
<template>
<ul class="events" ref="events" :class="{ 'disable-wrap': !softWrap, [size]: true }">
<ul class="events group py-4" :class="{ 'disable-wrap': !softWrap, [size]: true, compact }">
<li
v-for="(item, index) in filtered"
v-for="item in messages"
:key="item.id"
:data-key="item.id"
:class="{ selected: toRaw(item) === toRaw(lastSelectedItem) }"
:class="{ 'border border-secondary': toRaw(item) === toRaw(lastSelectedItem) }"
class="group/entry"
>
<div class="line-options" v-show="isSearching()">
<dropdown-menu :class="{ 'is-last': index === filtered.length - 1 }" class="is-top minimal">
<a class="dropdown-item" @click="handleJumpLineSelected($event, item)" :href="`#${item.id}`">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<cil:find-in-page class="mr-4" />
</div>
</div>
<div class="level-right">
<div class="level-item">Jump to Context</div>
</div>
</div>
</a>
</dropdown-menu>
</div>
<component :is="item.getComponent()" :log-entry="item" :visible-keys="visibleKeys.value"></component>
<component :is="item.getComponent()" :log-entry="item" :visible-keys="visibleKeys" />
</li>
</ul>
</template>
<script lang="ts" setup>
import { type ComputedRef, toRaw } from "vue";
import { useRouteHash } from "@vueuse/router";
import { Container } from "@/models/Container";
import { toRaw } from "vue";
import { type JSONObject, LogEntry } from "@/models/LogEntry";
const props = defineProps<{
defineProps<{
messages: LogEntry<string | JSONObject>[];
visibleKeys: string[][];
lastSelectedItem: LogEntry<string | JSONObject> | undefined;
}>();
let visibleKeys = persistentVisibleKeys(inject("container") as ComputedRef<Container>);
const { filteredPayload } = useVisibleFilter(visibleKeys);
const { filteredMessages, resetSearch, isSearching } = useSearchFilter();
const { messages } = toRefs(props);
const visible = filteredPayload(messages);
const filtered = filteredMessages(visible);
const events = ref<HTMLElement>();
let lastSelectedItem: LogEntry<string | JSONObject> | undefined = $ref(undefined);
function handleJumpLineSelected(e: Event, item: LogEntry<string | JSONObject>) {
lastSelectedItem = item;
resetSearch();
}
const routeHash = useRouteHash();
watch(
routeHash,
(hash) => {
if (hash) {
document.querySelector(`[data-key="${hash.substring(1)}"]`)?.scrollIntoView({ block: "center" });
}
},
{ immediate: true, flush: "post" },
);
</script>
<style scoped lang="scss">
<style scoped lang="postcss">
.events {
padding: 1em 0;
font-family:
ui-monospace,
SFMono-Regular,
@@ -78,41 +35,33 @@ watch(
Menlo,
monospace;
& > li {
display: flex;
word-wrap: break-word;
padding: 0.2em 1em;
> li {
@apply flex break-words px-2 py-1 last:snap-end odd:bg-gray-400/[0.07] md:px-4;
&:last-child {
scroll-snap-align: end;
scroll-margin-block-end: 5rem;
}
&:nth-child(odd) {
background-color: rgba(125, 125, 125, 0.08);
}
&.selected {
border: 1px var(--secondary-color) solid;
}
& > .line-options {
display: flex;
flex-direction: row-reverse;
margin-right: 1em;
.jump-context {
@apply mr-2 flex items-center font-sans text-secondary;
}
}
&.small {
font-size: 60%;
@apply text-[0.7em];
}
&.medium {
font-size: 80%;
@apply text-[0.8em];
}
&.large {
font-size: 120%;
@apply text-lg;
}
&.compact {
> li {
@apply py-0;
}
}
}
</style>
@@ -1,6 +1,6 @@
<template>
<log-event-source ref="source" #default="{ messages }" @loading-more="loadingMore($event)">
<log-viewer :messages="messages"></log-viewer>
<container-log-viewer :messages="messages" />
</log-event-source>
</template>
+14 -26
View File
@@ -1,38 +1,26 @@
<template>
<div class="columns is-1 is-variable is-mobile">
<div class="column is-narrow" v-if="showStd">
<log-std :std="logEntry.std"></log-std>
</div>
<div class="column is-narrow" v-if="showTimestamp">
<log-date :date="logEntry.date"></log-date>
</div>
<div class="column is-narrow is-flex">
<log-level :level="logEntry.level" :position="logEntry.position"></log-level>
</div>
<div class="text column" v-html="colorize(logEntry.message)"></div>
<div class="relative flex w-full items-start gap-x-2">
<log-std :std="logEntry.std" v-if="showStd" />
<log-date :date="logEntry.date" v-if="showTimestamp" />
<log-level class="flex" :level="logEntry.level" :position="logEntry.position" />
<div
class="whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap"
v-html="markSearch(logEntry.message)"
></div>
<log-message-actions
class="duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100"
:message="decodeXML(logEntry.message)"
:log-entry="logEntry"
/>
</div>
</template>
<script lang="ts" setup>
import { SimpleLogEntry } from "@/models/LogEntry";
import AnsiConvertor from "ansi-to-html";
import { decodeXML } from "entities";
const ansiConvertor = new AnsiConvertor({ escapeXML: false, fg: "var(--text-color)" });
defineProps<{
logEntry: SimpleLogEntry;
}>();
const { markSearch } = useSearchFilter();
const colorize = (value: string) => markSearch(ansiConvertor.toHtml(value));
</script>
<style lang="scss" scoped>
.disable-wrap {
.text {
white-space: nowrap;
}
}
.text {
white-space: pre-wrap;
}
</style>
@@ -1,8 +1,10 @@
<template>
<div class="is-flex-grow-1 has-text-centered my-4">
<div class="is-relative">
<zig-zag class="is-overlay mt-2"></zig-zag>
<span class="text is-relative py-2 px-4">{{ $t("error.logs-skipped", { total: logEntry.totalSkipped }) }}</span>
<div class="my-4 flex-1 text-center">
<div class="relative">
<zig-zag class="absolute inset-0 mt-2"></zig-zag>
<span class="relative whitespace-pre-wrap bg-base px-4 py-2 font-bold">{{
$t("error.logs-skipped", { total: logEntry.totalSkipped })
}}</span>
</div>
</div>
</template>
@@ -13,11 +15,3 @@ defineProps<{
logEntry: SkippedLogsEntry;
}>();
</script>
<style lang="scss" scoped>
.text {
font-weight: bold;
white-space: pre-wrap;
background-color: var(--body-background-color);
}
</style>
+6 -33
View File
@@ -1,13 +1,13 @@
<template>
<div class="has-text-centered is-relative host" @mouseenter="mouseOver = true" @mouseleave="mouseOver = false">
<div class="has-border has-boxshadow is-hidden-mobile">
<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">
<stat-sparkline :data="data" @selected-point="onSelectedPoint"></stat-sparkline>
</div>
<div class="has-background-body-color is-top-left">
<span class="has-text-weight-light">{{ label }}</span>
<span class="has-text-weight-bold">
<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 }}
</span>
</div>
</div>
</div>
</template>
@@ -23,30 +23,3 @@ function onSelectedPoint(point: Point<unknown>) {
let mouseOver = $ref(false);
</script>
<style lang="scss" scoped>
.has-border {
border: 1px solid var(--primary-color);
border-radius: 3px;
padding: 1px 1px 0 1px;
display: flex;
overflow: hidden;
padding-top: 0.25em;
}
.has-background-body-color {
background-color: var(--body-background-color);
}
.host:hover span {
color: var(--secondary-color);
}
@media screen and (min-width: 768px) {
.is-top-left {
position: absolute;
top: 0;
left: 0.75em;
}
}
</style>
+4 -20
View File
@@ -1,7 +1,7 @@
<template>
<svg :width="width" :height="height" @mousemove="onMove">
<path :d="path" class="area" />
<line :x1="lineX" y1="0" :x2="lineX" :y2="height" class="line" />
<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>
@@ -12,7 +12,7 @@ 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([width, 0]);
const x = d3.scaleLinear().range([0, width]);
const y = d3.scaleLinear().range([height, 0]);
const selectedPoint = defineEmit<[value: Point<unknown>]>();
@@ -42,19 +42,3 @@ function onMove(e: MouseEvent) {
selectedPoint(point);
}
</script>
<style scoped>
:deep(.area) {
fill: var(--primary-color);
}
:deep(.line) {
stroke: var(--secondary-color);
stroke-width: 2;
display: none;
}
svg:hover :deep(.line) {
display: unset;
}
</style>
+2 -10
View File
@@ -2,18 +2,10 @@
<svg width="100%" height="8" class="zigzag">
<defs>
<pattern id="zigzag" x="0" y="0" width="30" height="8" patternUnits="userSpaceOnUse">
<line x1="0" y1="0" x2="15" y2="8" class="line" />
<line x1="15" y1="8" x2="30" y2="0" class="line" />
<line x1="0" y1="0" x2="15" y2="8" class="stroke-primary stroke-1" />
<line x1="15" y1="8" x2="30" y2="0" class="stroke-primary stroke-1" />
</pattern>
</defs>
<rect x="0" y="0" width="100%" height="100%" fill="url(#zigzag)"></rect>
</svg>
</template>
<style lang="scss" scoped>
.line {
stroke: var(--primary-color);
stroke-width: 1;
stroke-linecap: round;
}
</style>
@@ -1,237 +1,105 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<LogEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
"<ul data-v-2e92daca="" class="events group py-4 medium">
<li data-v-2e92daca="" data-key="1" class="group/entry">
<div data-v-2e92daca="" class="relative flex w-full items-start gap-x-2" visible-keys="">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
<div data-v-961504e7="" class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] text-sm" 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>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
<div data-v-e625cddd="" class="mt-1.5 size-2.5 flex-none rounded-lg flex"></div>
<div class="whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap">&lt;test&gt;foo bar&lt;/test&gt;</div>
<div class="flex gap-2 duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render dates with 24 hour style 1`] = `
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
"<ul data-v-2e92daca="" class="events group py-4 medium">
<li data-v-2e92daca="" data-key="1" class="group/entry">
<div data-v-2e92daca="" class="relative flex w-full items-start gap-x-2" visible-keys="">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42</time></div>
<div data-v-961504e7="" class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] text-sm" 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>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
<div data-v-e625cddd="" class="mt-1.5 size-2.5 flex-none rounded-lg flex"></div>
<div class="whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap">&lt;test&gt;foo bar&lt;/test&gt;</div>
<div class="flex gap-2 duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages 1`] = `
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
"<ul data-v-2e92daca="" class="events group py-4 medium">
<li data-v-2e92daca="" data-key="1" class="group/entry">
<div data-v-2e92daca="" class="relative flex w-full items-start gap-x-2" visible-keys="">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
<div data-v-961504e7="" class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] text-sm" 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>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
<div data-v-e625cddd="" class="mt-1.5 size-2.5 flex-none rounded-lg flex"></div>
<div class="whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap">This is a message.</div>
<div class="flex gap-2 duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\">This is a message.</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with color 1`] = `
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\"><span style=\\"color:#000\\">black<span style=\\"color:#AAA\\">white</span></span></div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with filter 1`] = `
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"2\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#2\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
"<ul data-v-2e92daca="" class="events group py-4 medium">
<li data-v-2e92daca="" data-key="2" class="group/entry">
<div data-v-2e92daca="" class="relative flex w-full items-start gap-x-2" visible-keys="">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
<div data-v-961504e7="" class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] text-sm" 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>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
<div data-v-e625cddd="" class="mt-1.5 size-2.5 flex-none rounded-lg flex"></div>
<div class="whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap"><mark>test</mark> bar</div>
<div 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 min-w-[0.98rem] items-start justify-end align-bottom hover:cursor-pointer" title="log_actions.jump_to_context"><a class="rounded bg-slate-800/60 px-1.5 py-1 text-primary hover:bg-slate-700" href="#2"><svg viewBox="0 0 32 32" width="1.2em" height="1.2em">
<path fill="currentColor" d="m30 28.586l-4.688-4.688a8.028 8.028 0 1 0-1.415 1.414L28.586 30zM19 25a6 6 0 1 1 6-6a6.007 6.007 0 0 1-6 6M2 12h8v2H2zM2 2h16v2H2zm0 5h16v2H2z"></path>
</svg></a></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\"><mark>test</mark> bar</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with html entities 1`] = `
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
"<ul data-v-2e92daca="" class="events group py-4 medium">
<li data-v-2e92daca="" data-key="1" class="group/entry">
<div data-v-2e92daca="" class="relative flex w-full items-start gap-x-2" visible-keys="">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
<div data-v-961504e7="" class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] text-sm" 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>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
<div data-v-e625cddd="" class="mt-1.5 size-2.5 flex-none rounded-lg flex"></div>
<div class="whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap">&lt;test&gt;foo bar&lt;/test&gt;</div>
<div class="flex gap-2 duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > renders correctly 1`] = `
"<div data-v-1cd63c6e=\\"\\" class=\\"infinte-loader\\">
<div data-v-1cd63c6e=\\"\\" class=\\"spinner\\" style=\\"display: none;\\">
<div data-v-1cd63c6e=\\"\\" class=\\"bounce1\\"></div>
<div data-v-1cd63c6e=\\"\\" class=\\"bounce2\\"></div>
<div data-v-1cd63c6e=\\"\\" class=\\"bounce3\\"></div>
</div>
</div>
<ul data-v-2e92daca=\\"\\" class=\\"events medium\\"></ul>"
"<div class="flex min-h-[1px] justify-center"><span class="loading loading-bars loading-md mt-4 text-primary" style="display: none;"></span></div>
<ul data-v-2e92daca="" class="events group py-4 medium"></ul>"
`;
exports[`<LogEventSource /> > should parse messages 1`] = `
+12
View File
@@ -0,0 +1,12 @@
<template>
<div class="flex flex-col gap-8 px-4 py-4 md:px-8">
<section>
<links />
</section>
<slot></slot>
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>
+28 -21
View File
@@ -1,25 +1,28 @@
<template>
<div @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" ref="trigger"><slot></slot></div>
<Teleport to="body">
<Transition name="fade">
<div v-show="show && (delayedShow || glopbalShow)" class="content" ref="content">
<slot></slot>
<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"
ref="content"
>
<slot name="content"></slot>
</div>
</Transition>
</Teleport>
</transition>
</teleport>
</template>
<script lang="ts" setup>
import { globalShowPopup } from "@/composables/popup";
import { globalShowPopup } from "@/composable/popup";
let glopbalShow = globalShowPopup();
let show = ref(glopbalShow.value);
let delayedShow = refDebounced(show, 1000);
let content: HTMLElement | null = $ref(null);
let trigger: HTMLElement | null = $ref(null);
function onMouseEnter(e: MouseEvent) {
const onMouseEnter = (e: Event) => {
show.value = true;
glopbalShow.value = true;
if (e.target && content && e.target instanceof Element) {
@@ -30,22 +33,26 @@ function onMouseEnter(e: MouseEvent) {
content.style.left = `${x}px`;
content.style.top = `${y}px`;
}
}
};
function onMouseLeave() {
const onMouseLeave = () => {
show.value = false;
glopbalShow.value = false;
}
};
const el = useCurrentElement();
useEventListener(() => el.value?.nextElementSibling, "mouseenter", onMouseEnter);
useEventListener(() => el.value?.nextElementSibling, "mouseleave", onMouseLeave);
</script>
<style scoped>
.content {
position: fixed;
z-index: 9999;
background: var(--scheme-main-ter);
border-radius: 0.5em;
padding: 1em;
box-shadow: 0 0 0.5em rgba(0, 0, 0, 0.5);
border: 1px solid var(--border-color);
<style scoped lang="postcss">
.fade-enter-active,
.fade-leave-active {
@apply transition-opacity;
}
.fade-enter-from,
.fade-leave-to {
@apply opacity-0;
}
</style>
+74
View File
@@ -0,0 +1,74 @@
<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>
+28 -36
View File
@@ -1,18 +1,18 @@
<template>
<transition name="fade">
<div class="scroll-progress" ref="root" v-show="!autoHide || show">
<transition name="fadeout">
<div class="pointer-events-none relative inline-block" ref="root" v-show="!autoHide || show">
<svg width="100" height="100" viewBox="0 0 100 100" :class="{ indeterminate }">
<circle r="44" cx="50" cy="50" />
<circle r="44" cx="50" cy="50" class="fill-base-darker stroke-primary" />
</svg>
<div class="is-overlay columns is-vcentered is-centered has-text-weight-light">
<div class="absolute inset-0 flex items-center justify-center font-light">
<template v-if="indeterminate">
<div class="column is-narrow is-paddingless is-size-2">&#8734;</div>
<div class="text-4xl">&#8734;</div>
</template>
<template v-else-if="!isNaN(scrollProgress)">
<span class="column is-narrow is-paddingless is-size-2">
<span class="text-4xl">
{{ Math.ceil(scrollProgress * 100) }}
</span>
<span class="column is-narrow is-paddingless"> % </span>
<span> % </span>
</template>
</div>
</div>
@@ -54,36 +54,28 @@ watchPostEffect(() => {
}
});
</script>
<style scoped lang="scss">
.scroll-progress {
display: inline-block;
position: relative;
pointer-events: none;
<style scoped lang="postcss">
svg {
filter: drop-shadow(0px 1px 1px rgba(0, 0, 0, 0.2));
margin-top: 5px;
&.indeterminate {
animation: 2s linear infinite svg-animation;
svg {
filter: drop-shadow(0px 1px 1px rgba(0, 0, 0, 0.2));
margin-top: 5px;
&.indeterminate {
animation: 2s linear infinite svg-animation;
circle {
animation: 1.4s ease-in-out infinite both circle-animation;
}
}
circle {
fill: var(--scheme-main-ter);
fill-opacity: 0.8;
transition: stroke-dashoffset 250ms ease-out;
transform: rotate(-90deg);
transform-origin: 50% 50%;
stroke: var(--primary-color);
stroke-dashoffset: calc(276.32px - v-bind(scrollProgress) * 276.32px);
stroke-dasharray: 276.32px 276.32px;
stroke-linecap: round;
stroke-width: 3;
will-change: stroke-dashoffset;
animation: 1.4s ease-in-out infinite both circle-animation;
}
}
circle {
fill-opacity: 0.75;
transition: stroke-dashoffset 250ms ease-out;
transform: rotate(-90deg);
transform-origin: 50% 50%;
stroke-dashoffset: calc(276.32px - v-bind(scrollProgress) * 276.32px);
stroke-dasharray: 276.32px 276.32px;
stroke-linecap: round;
stroke-width: 3;
will-change: stroke-dashoffset;
}
}
@keyframes svg-animation {
@@ -113,11 +105,11 @@ watchPostEffect(() => {
}
}
.fade-leave-active {
transition: opacity 0.2s ease-in-out;
.fadeout-leave-active {
@apply transition-opacity;
}
.fade-leave-to {
.fadeout-leave-to {
opacity: 0;
}
</style>
+23 -106
View File
@@ -1,28 +1,31 @@
<template>
<section :class="{ 'is-full-height-scrollable': scrollable }">
<header v-if="$slots.header">
<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"
>
<slot name="header"></slot>
</header>
<main :data-scrolling="scrollable ? true : undefined">
<div class="is-scrollbar-progress is-hidden-mobile">
<scroll-progress v-if="paused" :indeterminate="loading" :auto-hide="!loading"></scroll-progress>
<main :data-scrolling="scrollable ? true : undefined" class="snap-y overflow-auto">
<div class="invisible mr-28 text-right md:visible" v-show="paused">
<scroll-progress :indeterminate="loading" :auto-hide="!loading" class="!fixed top-16 z-10" />
</div>
<div ref="scrollableContent">
<slot :setLoading="setLoading"></slot>
</div>
<div ref="scrollObserver" class="is-scroll-observer"></div>
<div ref="scrollObserver" class="h-px"></div>
</main>
<div class="is-scrollbar-notification">
<div class="mr-16 text-right">
<transition name="fade">
<button
class="button has-boxshadow"
:class="hasMore ? 'has-more' : ''"
class="btn btn-primary fixed bottom-8 rounded p-3 text-primary-content shadow transition-colors"
:class="hasMore ? 'btn-secondary animate-bounce-fast text-secondary-content' : ''"
@click="scrollToBottom()"
v-show="paused"
>
<mdi:light-chevron-double-down />
<mdi:chevron-double-down />
</button>
</transition>
</div>
@@ -71,106 +74,20 @@ function setLoading(value: boolean) {
loading = value;
}
</script>
<style scoped lang="scss">
section {
display: flex;
flex-direction: column;
<style scoped lang="postcss">
.fade-enter-active,
.fade-leave-active {
@apply transition-opacity;
}
header {
position: sticky;
top: 0;
background: var(--body-background-color);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
z-index: 1;
@media screen and (max-width: 768px) {
top: 70px;
}
}
&.is-full-height-scrollable {
height: 100vh;
min-height: 0;
}
main {
flex: 1;
overflow: auto;
scroll-snap-type: y proximity;
}
.is-scrollbar-progress {
text-align: right;
margin-right: 110px;
.scroll-progress {
position: fixed;
top: 60px;
z-index: 2;
}
}
.is-scroll-observer {
height: 1px;
}
.is-scrollbar-notification {
text-align: right;
margin-right: 65px;
button {
position: fixed;
bottom: 30px;
background-color: var(--primary-color);
transition: background-color 0.24s ease-out;
border: none !important;
color: #eee;
&.has-more {
background-color: var(--secondary-color);
animation-name: bounce;
animation-duration: 1000ms;
animation-fill-mode: both;
color: #222;
}
}
}
@keyframes bounce {
0%,
20%,
50%,
80%,
100% {
transform: translateY(0);
}
40% {
transform: translateY(-30px);
}
60% {
transform: translateY(-15px);
}
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease-out !important;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.fade-enter-from,
.fade-leave-to {
@apply opacity-0;
}
</style>
<style>
@media screen and (max-width: 768px) {
.splitpanes__pane {
overflow: unset !important;
}
.splitpanes__pane {
overflow: unset !important;
}
</style>
+39 -51
View File
@@ -1,30 +1,35 @@
<template>
<div class="search columns is-gapless is-vcentered" v-show="showSearch" v-if="search">
<div class="column">
<p class="control has-icons-left">
<transition name="slide">
<div
class="fixed z-10 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">
<mdi:magnify />
<input
class="input"
class="input input-ghost w-72 flex-1"
type="text"
placeholder="Find / RegEx"
ref="input"
v-model="searchFilter"
@keyup.esc="resetSearch()"
/>
<span class="icon is-left">
<mdi:light-magnify />
</span>
</p>
<a class="btn btn-circle btn-xs" @click="resetSearch()"> <mdi:close /></a>
</div>
</div>
<div class="column is-1 has-text-centered">
<button class="delete is-medium" @click="resetSearch()"></button>
</div>
</div>
</transition>
</template>
<script lang="ts" setup>
const input = ref<HTMLInputElement>();
const container = ref<HTMLDivElement>();
const { searchFilter, showSearch, resetSearch } = useSearchFilter();
const { style } = useDraggable(container);
onKeyStroke("f", (e) => {
if (e.ctrlKey || e.metaKey) {
showSearch.value = true;
@@ -33,48 +38,31 @@ onKeyStroke("f", (e) => {
}
});
onMounted(() => {
onKeyStroke(
"f",
(e) => {
if (e.ctrlKey || e.metaKey) {
e.stopPropagation();
resetSearch();
}
},
{ target: input.value },
);
});
onUnmounted(() => resetSearch());
</script>
<style lang="scss" scoped>
.search {
width: 350px;
position: fixed;
padding: 10px;
background: var(--scheme-main-ter);
top: 0;
right: 0;
border-radius: 0 0 0 5px;
z-index: 10;
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.12),
0 1px 2px rgba(0, 0, 0, 0.24);
<style lang="postcss" scoped>
.slide-enter-active,
.slide-leave-active {
transition: all 200ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
button.delete {
margin-left: 1em;
background-color: var(--scheme-main-ter);
opacity: 0.6;
&:after,
&:before {
background-color: var(--text-color);
}
&:hover {
opacity: 1;
}
}
.icon {
padding: 10px 3px;
}
.input {
color: var(--body-color);
&::placeholder {
color: var(--border-color);
}
}
.slide-enter-from,
.slide-leave-to {
transform: translateY(-150px);
opacity: 0;
}
</style>
+60 -68
View File
@@ -1,69 +1,69 @@
<template>
<div v-if="ready">
<nav class="breadcrumb menu-label" aria-label="breadcrumbs">
<ul v-if="sessionHost">
<li>
<a href="#" @click.prevent="setHost(null)">{{ hosts[sessionHost].name }}</a>
<div v-if="ready" data-testid="side-menu">
<div class="breadcrumbs">
<ul>
<li><a @click.prevent="setHost(null)" class="link-primary">Hosts</a></li>
<li v-if="sessionHost && hosts[sessionHost]" class="cursor-default">
{{ hosts[sessionHost].name }}
</li>
</ul>
<ul v-else>
<li>Hosts</li>
</ul>
</nav>
</div>
<transition :name="sessionHost ? 'slide-left' : 'slide-right'" mode="out-in">
<ul class="menu-list" v-if="!sessionHost">
<ul class="menu p-0" v-if="!sessionHost">
<li v-for="host in config.hosts">
<a @click.prevent="setHost(host.id)">{{ host.name }}</a>
<a @click.prevent="setHost(host.id)">
<ph:computer-tower />
{{ host.name }}
</a>
</li>
</ul>
<transition-group tag="ul" name="list" class="menu-list" v-else>
<li v-for="item in menuItems" :key="item.id" :class="item.state" :data-label="item.id">
<div class="menu-label mt-4 mb-3" v-if="isLabel(item)">
{{ item.label }}
</div>
<popup v-else>
<transition-group tag="ul" name="list" class="containers menu p-0 [&_li.menu-title]:px-0" v-else>
<li
v-for="item in menuItems"
:key="isContainer(item) ? item.id : item.keyLabel"
:class="isContainer(item) ? item.state : 'menu-title'"
:data-testid="isContainer(item) ? null : item.keyLabel"
>
<popup v-if="isContainer(item)">
<router-link
:to="{ name: 'container-id', params: { id: item.id } }"
active-class="is-active"
active-class="active-primary"
@click.alt.stop.prevent="store.appendActiveContainer(item)"
:title="item.name"
>
<div class="container is-flex is-align-items-center">
<div class="is-flex-grow-1 is-ellipsis">
<span>{{ item.name }}</span
><span class="has-text-weight-light has-light-opacity" v-if="item.isSwarm">{{ item.swarmId }}</span>
</div>
<div class="is-flex-shrink-1 is-flex icons">
<div
class="icon is-small pin"
@click.stop.prevent="store.appendActiveContainer(item)"
v-show="!activeContainersById[item.id]"
:title="$t('tooltip.pin-column')"
>
<cil:columns />
</div>
<container-health :health="item.health"></container-health>
</div>
<div class="truncate">
{{ item.name }}<span class="font-light opacity-70" v-if="item.isSwarm">{{ item.swarmId }}</span>
</div>
<container-health :health="item.health"></container-health>
<span
class="pin"
@click.stop.prevent="store.appendActiveContainer(item)"
v-show="!activeContainersById[item.id]"
:title="$t('tooltip.pin-column')"
>
<cil:columns />
</span>
</router-link>
<template #content>
<container-popup :container="item"></container-popup>
</template>
</popup>
<template v-else>
{{ $t(item.keyLabel) }}
</template>
</li>
</transition-group>
</transition>
</div>
<ul class="menu-list is-hidden-mobile has-light-opacity" v-else>
<li v-for="index in 7" class="my-4"><o-skeleton animated size="large" :key="index"></o-skeleton></li>
</ul>
<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>
<span class="sr-only">Loading...</span>
</div>
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
import { sessionHost } from "@/composables/storage";
const { t } = useI18n();
import { sessionHost } from "@/composable/storage";
const store = useContainerStore();
@@ -91,7 +91,7 @@ const sortedContainers = computed(() =>
const groupedContainers = computed(() =>
sortedContainers.value.reduce(
(acc, item) => {
if (debouncedIds.value.has(item.storageKey)) {
if (debouncedIds.value.has(item.name)) {
acc.pinned.push(item);
} else {
acc.unpinned.push(item);
@@ -102,15 +102,13 @@ const groupedContainers = computed(() =>
),
);
type MenuLabel = { label: string; id: string; state: string };
const pinnedLabel = { label: t("label.pinned"), id: "pinned", state: "label" } as MenuLabel;
const allLabel = { label: t("label.containers"), id: "all", state: "label" } as MenuLabel;
function isLabel(item: Container | MenuLabel): item is MenuLabel {
return (item as MenuLabel).label !== undefined;
function isContainer(item: any): item is Container {
return item.hasOwnProperty("image");
}
const menuItems = computed(() => {
const pinnedLabel = { keyLabel: "label.pinned" };
const allLabel = { keyLabel: showAllContainers.value ? "label.all-containers" : "label.running-containers" };
if (groupedContainers.value.pinned.length > 0) {
return [pinnedLabel, ...groupedContainers.value.pinned, allLabel, ...groupedContainers.value.unpinned];
} else {
@@ -138,35 +136,29 @@ const activeContainersById = computed(() =>
),
);
</script>
<style scoped lang="scss">
.has-light-opacity {
opacity: 0.5;
<style scoped lang="postcss">
.menu {
@apply text-[0.95rem];
}
li.exited a,
li.dead a {
color: #777;
}
.icons {
column-gap: 0.35em;
align-items: baseline;
}
a {
.containers a {
@apply auto-cols-[auto_max-content_max-content];
.pin {
display: none;
&:hover {
color: var(--secondary-color);
@apply text-secondary;
}
}
&:hover {
.pin {
display: block;
display: inline-block;
}
}
}
li.exited {
@apply opacity-50;
}
.slide-left-enter-active,
.slide-left-leave-active,
@@ -177,22 +169,22 @@ a {
.slide-left-enter-from {
opacity: 0;
transform: translateX(100%);
transform: translateX(20px);
}
.slide-right-enter-from {
opacity: 0;
transform: translateX(-100%);
transform: translateX(-20px);
}
.slide-left-leave-to {
opacity: 0;
transform: translateX(-100%);
transform: translateX(-20px);
}
.slide-right-leave-to {
opacity: 0;
transform: translateX(100%);
transform: translateX(20px);
}
.list-move,
+23 -63
View File
@@ -1,71 +1,31 @@
<template>
<aside>
<div class="columns is-marginless">
<div class="column is-paddingless">
<h1>
<router-link :to="{ name: 'index' }">
<svg class="logo">
<use href="#logo"></use>
</svg>
</router-link>
<aside class="fixed h-screen w-[inherit] overflow-auto p-3" data-testid="navigation">
<h1>
<router-link :to="{ name: 'index' }">
<svg class="h-14 w-28 fill-secondary">
<use href="#logo"></use>
</svg>
</router-link>
<small class="mb-4 block text-xs font-light" v-if="hostname">
{{ hostname }}
</small>
</h1>
<a
class="input input-sm mt-4 inline-flex cursor-pointer items-center gap-2 font-light hover:border-primary"
@click="$emit('search')"
:title="$t('tooltip.search')"
>
<mdi:magnify />
Search
<key-shortcut char="k"></key-shortcut>
</a>
<small class="subtitle is-6 is-block mb-4" v-if="hostname">
{{ hostname }}
</small>
</h1>
</div>
</div>
<div class="columns is-marginless">
<div class="column is-narrow py-0 pl-0 pr-1">
<button class="button is-rounded is-small" @click="$emit('search')" :title="$t('tooltip.search')">
<span class="icon">
<mdi:light-magnify />
</span>
</button>
</div>
<div class="column is-narrow py-0" :class="secured ? 'pl-0 pr-1' : 'px-0'">
<router-link
:to="{ name: 'settings' }"
active-class="is-active"
class="button is-rounded is-small"
:aria-label="$t('title.settings')"
>
<span class="icon">
<mdi:light-cog />
</span>
</router-link>
</div>
<div class="column is-narrow py-0 px-0" v-if="secured">
<a class="button is-rounded is-small" :href="`${base}/logout`" :title="$t('button.logout')">
<span class="icon">
<mdi:light-logout />
</span>
</a>
</div>
</div>
<side-menu class="mt-4"></side-menu>
</aside>
</template>
<script lang="ts" setup>
const { base, secured, hostname } = config;
const { hostname } = config;
</script>
<style scoped lang="scss">
aside {
padding: 1em;
height: 100vh;
overflow: auto;
position: fixed;
width: inherit;
.is-hidden-mobile.is-active {
display: block !important;
}
}
.logo {
width: 122px;
height: 54px;
fill: var(--logo-color);
}
</style>
+25 -8
View File
@@ -1,6 +1,6 @@
<template>
<div>
<time :datetime="date.toISOString()" class="is-hidden-mobile">{{ dateStr }}</time>
<div class="inline-flex gap-2">
<time :datetime="date.toISOString()" class="mobile-hidden">{{ dateStr }}</time>
<time :datetime="date.toISOString()">{{ timeStr }}</time>
</div>
</template>
@@ -10,13 +10,30 @@ const props = defineProps<{
date: Date;
}>();
const dateFormatter = new Intl.DateTimeFormat(undefined, { day: "2-digit", month: "2-digit", year: "numeric" });
const use12Hour = $computed(() => ({ auto: undefined, "12": true, "24": false })[hourStyle.value]);
const timeFormatter = $computed(
const dateOverride = computed(() => (dateLocale.value === "auto" ? undefined : dateLocale.value));
const dateFormatter = computed(
() => new Intl.DateTimeFormat(dateOverride.value, { day: "2-digit", month: "2-digit", year: "numeric" }),
);
const hourCycle = computed(() => {
switch (hourStyle.value) {
case "auto":
return undefined;
case "12":
return "h12";
case "24":
return "h23";
}
});
const timeFormatter = computed(
() =>
new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit", second: "2-digit", hour12: use12Hour }),
new Intl.DateTimeFormat(undefined, {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hourCycle: hourCycle.value,
}),
);
const dateStr = $computed(() => dateFormatter.format(props.date));
const timeStr = $computed(() => timeFormatter.format(props.date));
const dateStr = computed(() => dateFormatter.value.format(props.date));
const timeStr = computed(() => timeFormatter.value.format(props.date));
</script>
+2 -2
View File
@@ -3,8 +3,8 @@
</template>
<script lang="ts" setup>
import formatDistanceToNow from "date-fns/formatDistanceToNow";
import formatDistanceToNowStrict from "date-fns/formatDistanceToNowStrict";
import { formatDistanceToNow } from "date-fns/formatDistanceToNow";
import { formatDistanceToNowStrict } from "date-fns/formatDistanceToNowStrict";
const {
date,
+25
View File
@@ -0,0 +1,25 @@
<template>
<div class="dropdown">
<label tabindex="0" class="btn btn-circle btn-sm" @mousedown="checkAndCloseDropDown($event)" @blur="closed()">
<slot name="trigger"></slot>
</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"
>
<slot name="content"></slot>
</div>
</div>
</template>
<script setup lang="ts">
const closed = defineEmit();
function checkAndCloseDropDown(e: MouseEvent) {
const target = e.currentTarget as HTMLElement;
if (target?.matches(":focus")) {
setTimeout(() => target.blur(), 0);
}
}
</script>
<style scoped></style>
+40
View File
@@ -0,0 +1,40 @@
<template>
<details class="dropdown" 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">
<slot>
<li v-for="item in options">
<a @click="modelValue = item.value">
<mdi:check class="w-4" v-if="modelValue == item.value" />
<div v-else class="w-4"></div>
{{ item.label }}
</a>
</li>
</slot>
</ul>
</details>
</template>
<script lang="ts" setup generic="T">
import { vOnClickOutside } from "@vueuse/components";
type DropdownItem = {
label: string;
value: T;
};
const { modelValue } = defineModels<{
modelValue: T;
}>();
const { options = [], defaultLabel = "" } = defineProps<{
options?: DropdownItem[];
defaultLabel?: string;
}>();
const label = computed(() => options.find((item) => item.value === modelValue.value)?.label ?? defaultLabel);
const details = ref<HTMLElement | null>(null);
const close = () => details.value?.removeAttribute("open");
watch(modelValue, () => close());
</script>
+19
View File
@@ -0,0 +1,19 @@
<template>
<div class="inline-flex items-center">
<template v-if="modifiers.includes('shift')">
<carbon:mac-shift />
</template>
<template v-if="modifiers.includes('meta')">
<ph:command v-if="isMac" class="size-4" />
<ph:control-bold v-else class="size-4" />
</template>
<kbd class="uppercase">{{ char }}</kbd>
</div>
</template>
<script lang="ts">
const isMac = /(Mac|iPhone|iPod|iPad)/i.test(navigator.userAgent);
</script>
<script lang="ts" setup>
const { modifiers = ["meta"], char } = defineProps<{ char: string; modifiers?: ("^" | "meta" | "shift")[] }>();
</script>
+10
View File
@@ -0,0 +1,10 @@
<template>
<label class="label cursor-pointer gap-4">
<div class="label-text"><slot name="label" /></div>
<slot name="input" />
</label>
</template>
<script setup lang="ts"></script>
<style scoped></style>
+64 -130
View File
@@ -1,101 +1,64 @@
<template>
<aside>
<div class="columns is-marginless is-gapless is-mobile is-vcentered">
<div class="column is-narrow">
<router-link :to="{ name: 'index' }">
<svg class="logo">
<use href="#logo"></use>
</svg>
</router-link>
</div>
<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: 'index' }">
<svg class="h-14 w-28 fill-secondary">
<use href="#logo"></use>
</svg>
</router-link>
<div class="column is-narrow push-right">
<a
role="button"
class="navbar-burger burger is-hidden-tablet is-pulled-right"
@click="showNav = !showNav"
:class="{ 'is-active': showNav }"
>
<span></span> <span></span> <span></span>
<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>
<div class="menu-label level is-mobile is-hidden-mobile" :class="{ 'is-active': showNav }">
<div v-if="config.hosts.length > 1">
<o-dropdown v-model="sessionHost" aria-role="list">
<template #trigger>
<o-button variant="primary" type="button" size="small">
<span>{{ sessionHost ? hosts[sessionHost].name : "" }}</span>
<span class="icon">
<carbon:caret-down />
</span>
</o-button>
</template>
<o-dropdown-item :value="value.id" aria-role="listitem" v-for="value in config.hosts" :key="value">
<span>{{ value.name }}</span>
</o-dropdown-item>
</o-dropdown>
</div>
<div class="level-item has-text-centered">
<div>
<button class="button is-small is-rounded" @click="$emit('search')" :title="$t('tooltip.search')">
<span class="icon">
<mdi:light-magnify />
</span>
</button>
<transition name="fade">
<div v-show="show" class="h-[calc(100vh-60px)] overflow-auto">
<div class="mt-4 flex items-center justify-center gap-2">
<dropdown-menu
v-model="sessionHost"
:options="hosts"
defaultLabel="Hosts"
class="btn-sm"
v-if="config.hosts.length > 1"
/>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<router-link :to="{ name: 'settings' }" active-class="is-active" class="button is-small is-rounded">
<span class="icon">
<mdi:light-cog />
</span>
</router-link>
</div>
</div>
<div class="level-item has-text-centered" v-if="secured">
<div>
<a class="button is-small is-rounded" :href="`${base}/logout`" :title="$t('button.logout')">
<span class="icon">
<mdi:light-logout />
</span>
</a>
</div>
</div>
</div>
<p class="menu-label is-hidden-mobile" :class="{ 'is-active': showNav }">{{ $t("label.containers") }}</p>
<ul class="menu-list is-hidden-mobile" :class="{ 'is-active': showNav }">
<li v-for="item in sortedContainers" :key="item.id">
<router-link
:to="{ name: 'container-id', params: { id: item.id } }"
active-class="is-active"
:title="item.name"
>
<div class="is-ellipsis">
{{ item.name }}
</div>
</router-link>
</li>
</ul>
</aside>
<ul class="menu">
<li class="menu-title">{{ $t("label.containers") }}</li>
<li v-for="item in sortedContainers" :key="item.id" :class="item.state">
<router-link
:to="{ name: 'container-id', params: { id: item.id } }"
active-class="active-primary"
class="truncate"
:title="item.name"
>
{{ item.name }}
</router-link>
</li>
</ul>
</div>
</transition>
</nav>
</template>
<script lang="ts" setup>
const { base, secured } = config;
import { sessionHost } from "@/composables/storage";
import { sessionHost } from "@/composable/storage";
const store = useContainerStore();
const route = useRoute();
const { visibleContainers } = storeToRefs(store);
let showNav = $ref(false);
const show = ref(false);
watch(route, () => {
showNav = false;
show.value = false;
});
const sortedContainers = computed(() =>
@@ -112,59 +75,30 @@ const sortedContainers = computed(() =>
}),
);
const hosts = computed(() =>
config.hosts.reduce(
(acc, item) => {
acc[item.id] = item;
return acc;
},
{} as Record<string, { name: string; id: string }>,
),
);
const hosts = computed(() => config.hosts.map(({ id, name }) => ({ value: id, label: name })));
</script>
<style scoped lang="scss">
aside {
padding: 1em;
position: fixed;
left: 0;
right: 0;
background: var(--scheme-main-ter);
z-index: 10;
max-height: 100vh;
overflow: auto;
<style scoped lang="postcss">
li.exited {
@apply opacity-50;
}
.level.is-hidden-mobile.is-active {
display: flex !important;
}
.fade-enter-active,
.fade-leave-active {
@apply transition-opacity;
}
.menu-label {
margin-top: 1em;
}
.fade-enter-active .menu,
.fade-leave-active .menu {
@apply transition-transform;
}
.title {
text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.2);
}
.fade-enter-from,
.fade-leave-to {
@apply opacity-0;
}
.burger {
color: var(--body-color);
}
.is-hidden-mobile.is-active {
display: block !important;
}
.navbar-burger {
height: 2.35rem;
}
.logo {
width: 82px;
height: 36px;
fill: var(--logo-color);
}
.column.push-right {
margin-left: auto;
}
.fade-enter-from .menu,
.fade-leave-to .menu {
@apply -translate-y-2;
}
</style>
+4 -8
View File
@@ -1,5 +1,5 @@
<template>
<div class="tag" :size="size">
<div class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] text-sm" :size="size">
<slot></slot>
</div>
</template>
@@ -8,12 +8,8 @@
const { size = undefined } = defineProps<{ size?: "small" | undefined }>();
</script>
<style scoped lang="scss">
.tag {
background-color: var(--scheme-main-ter);
border: 1px solid var(--border-color);
&[size="small"] {
font-size: 0.61rem;
}
<style scoped lang="postcss">
[size="small"] {
@apply text-xs;
}
</style>
+39
View File
@@ -0,0 +1,39 @@
<template>
<button class="btn relative overflow-hidden" @click="cancel()">
<div class="absolute inset-0 origin-left bg-white/30" ref="progress"></div>
<div class="z-10">
<slot></slot>
</div>
</button>
</template>
<script lang="ts" setup>
const progress = ref<HTMLElement>();
const finished = defineEmit();
const cancelled = defineEmit();
let animation: Animation | undefined;
onMounted(async () => {
animation = progress.value?.animate([{ transform: "scaleX(0)" }, { transform: "scaleX(1)" }], {
duration: 4000,
easing: "linear",
fill: "forwards",
});
try {
await animation?.finished;
finished();
} catch (e) {
progress.value?.animate([{ transform: "scaleX(1)" }, { transform: "scaleX(0)" }], {
duration: 0,
fill: "forwards",
});
cancelled();
}
});
const cancel = () => {
animation?.cancel();
};
</script>
<style scoped lang="postcss"></style>
+16
View File
@@ -0,0 +1,16 @@
<template>
<labeled-input>
<template #label>
<slot />
</template>
<template #input>
<input type="checkbox" class="toggle toggle-primary" v-model="modelValue" />
</template>
</labeled-input>
</template>
<script lang="ts" setup>
const { modelValue } = defineModels<{
modelValue: boolean;
}>();
</script>
+45
View File
@@ -0,0 +1,45 @@
type ContainerActions = "start" | "stop" | "restart";
export const useContainerActions = () => {
const { container } = useContainerContext();
const { showToast } = useToast();
const actionStates = reactive({
stop: false,
restart: false,
start: false,
});
async function actionHandler(action: ContainerActions) {
const actionUrl = `/api/actions/${action}/${container.value.host}/${container.value.id}`;
const errors = {
404: "container not found",
500: "unable to complete action",
400: "invalid action",
} as Record<number, string>;
const defaultError = "something went wrong";
const toastTitle = "Action Failed";
actionStates[action] = true;
try {
const response = await fetch(withBase(actionUrl), { method: "POST" });
if (!response.ok) {
const message = errors[response.status] ?? defaultError;
showToast({ type: "error", message, title: toastTitle });
}
} catch (error) {
showToast({ type: "error", message: defaultError, title: toastTitle });
}
actionStates[action] = false;
}
return {
actionStates,
start: () => actionHandler("start"),
stop: () => actionHandler("stop"),
restart: () => actionHandler("restart"),
};
};
+23
View File
@@ -0,0 +1,23 @@
import { Container } from "@/models/Container";
type ContainerContext = {
container: Ref<Container>;
streamConfig: { stdout: boolean; stderr: boolean };
};
export const containerContext = Symbol("containerContext") as InjectionKey<ContainerContext>;
export const provideContainerContext = (container: Ref<Container>) => {
provide(containerContext, {
container,
streamConfig: reactive({ stdout: true, stderr: true }),
});
};
export const useContainerContext = () => {
const context = inject(containerContext);
if (!context) {
throw new Error("No container context provided");
}
return context;
};
@@ -1,4 +1,4 @@
import { type ComputedRef, type Ref } from "vue";
import { type Ref } from "vue";
import { encodeXML } from "entities";
import debounce from "lodash.debounce";
import {
@@ -9,7 +9,6 @@ import {
DockerEventLogEntry,
SkippedLogsEntry,
} from "@/models/LogEntry";
import { Container } from "@/models/Container";
function parseMessage(data: string): LogEntry<string | JSONObject> {
const e = JSON.parse(data, (key, value) => {
@@ -21,15 +20,12 @@ function parseMessage(data: string): LogEntry<string | JSONObject> {
return asLogEntry(e);
}
type LogStreamConfig = {
stdout: boolean;
stderr: boolean;
};
export function useLogStream(container: ComputedRef<Container>, streamConfig: LogStreamConfig) {
export function useLogStream() {
const { container, streamConfig } = useContainerContext();
let messages: LogEntry<string | JSONObject>[] = $ref([]);
let buffer: LogEntry<string | JSONObject>[] = $ref([]);
const scrollingPaused = $ref(inject("scrollingPaused") as Ref<boolean>);
let containerId = container.value.id;
function flushNow() {
if (messages.length > config.maxLogs) {
@@ -57,50 +53,54 @@ export function useLogStream(container: ComputedRef<Container>, streamConfig: Lo
}
const flushBuffer = debounce(flushNow, 250, { maxWait: 1000 });
let es: EventSource | null = null;
let lastEventId = "";
function close() {
if (es) {
es.close();
console.debug(`EventSource closed for ${containerId}`);
es = null;
}
}
function clearMessages() {
flushBuffer.cancel();
messages = [];
buffer = [];
console.debug(`Clearing messages for ${containerId}`);
}
function connect({ clear } = { clear: true }) {
es?.close();
close();
if (clear) {
flushBuffer.cancel();
messages = [];
buffer = [];
lastEventId = "";
clearMessages();
}
const params = {
lastEventId,
} as { lastEventId: string; stdout?: string; stderr?: string };
const params = Object.entries(streamConfig)
.filter(([, value]) => value)
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), {});
if (streamConfig.stdout) {
params.stdout = "1";
}
if (streamConfig.stderr) {
params.stderr = "1";
}
containerId = container.value.id;
console.debug(`Connecting to ${containerId} with params`, params);
es = new EventSource(
`${config.base}/api/logs/stream/${container.value.host}/${container.value.id}?${new URLSearchParams(
params,
).toString()}`,
withBase(`/api/logs/stream/${container.value.host}/${containerId}?${new URLSearchParams(params).toString()}`),
);
es.addEventListener("container-stopped", () => {
es?.close();
es = null;
close();
buffer.push(new DockerEventLogEntry("Container stopped", new Date(), "container-stopped"));
flushBuffer();
flushBuffer.flush();
});
es.addEventListener("error", (e) => console.error("EventSource failed: " + JSON.stringify(e)));
es.onmessage = (e) => {
lastEventId = e.lastEventId;
if (e.data) {
buffer.push(parseMessage(e.data));
flushBuffer();
}
};
es.onerror = () => clearMessages();
}
async function loadOlderLogs({ beforeLoading, afterLoading } = { beforeLoading: () => {}, afterLoading: () => {} }) {
@@ -112,23 +112,13 @@ export function useLogStream(container: ComputedRef<Container>, streamConfig: Lo
const delta = to.getTime() - last.getTime();
const from = new Date(to.getTime() + delta);
const params = {
from: from.toISOString(),
to: to.toISOString(),
} as { from: string; to: string; stdout?: string; stderr?: string };
if (streamConfig.stdout) {
params.stdout = "1";
}
if (streamConfig.stderr) {
params.stderr = "1";
}
const params = Object.entries(streamConfig)
.filter(([, value]) => value)
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), { from: from.toISOString(), to: to.toISOString() });
const logs = await (
await fetch(
`${config.base}/api/logs/${container.value.host}/${container.value.id}?${new URLSearchParams(
params,
).toString()}`,
withBase(`/api/logs/${container.value.host}/${containerId}?${new URLSearchParams(params).toString()}`),
)
).text();
if (logs) {
@@ -152,11 +142,7 @@ export function useLogStream(container: ComputedRef<Container>, streamConfig: Lo
},
);
onUnmounted(() => {
if (es) {
es.close();
}
});
onUnmounted(() => close());
watch(
() => container.value.id,
+16
View File
@@ -0,0 +1,16 @@
import { JSONObject, LogEntry } from "@/models/LogEntry";
const lastSelectedItem = ref<LogEntry<string | JSONObject> | undefined>(undefined);
export const useLogSearchContext = () => {
const { resetSearch } = useSearchFilter();
function handleJumpLineSelected(e: Event, item: LogEntry<string | JSONObject>) {
lastSelectedItem.value = item;
resetSearch();
}
return {
lastSelectedItem,
handleJumpLineSelected,
};
};
+36
View File
@@ -0,0 +1,36 @@
import { Profile } from "@/stores/config";
export function useProfileStorage<K extends keyof Profile>(key: K, defaultValue: NonNullable<Profile[K]>) {
const storageKey = "DOZZLE_" + key.toUpperCase();
const storage = useStorage<NonNullable<Profile[K]>>(storageKey, defaultValue, undefined, {
writeDefaults: false,
mergeDefaults: true,
});
if (config.profile?.[key]) {
if (storage.value instanceof Set && config.profile[key] instanceof Array) {
storage.value = new Set([...(config.profile[key] as Iterable<any>)]) as unknown as NonNullable<Profile[K]>;
} else if (config.profile[key] instanceof Array) {
storage.value = config.profile[key] as NonNullable<Profile[K]>;
} else if (config.profile[key] instanceof Object) {
Object.assign(storage.value, config.profile[key]);
} else {
storage.value = config.profile[key] as NonNullable<Profile[K]>;
}
}
if (config.user) {
watch(
storage,
(value) => {
fetch(withBase("/api/profile"), {
method: "PATCH",
body: JSON.stringify({ [key]: value }, (_, value) => (value instanceof Set ? [...value] : value)),
});
},
{ deep: true },
);
}
return storage;
}
+24
View File
@@ -0,0 +1,24 @@
import { Container } from "@/models/Container";
const DOZZLE_HOST = "DOZZLE_HOST";
export const sessionHost = useSessionStorage<string | null>(DOZZLE_HOST, null);
if (config.hosts.length === 1 && !sessionHost.value) {
sessionHost.value = config.hosts[0].id;
}
export function persistentVisibleKeys(container: Ref<Container>): Ref<string[][]> {
const storage = useProfileStorage("visibleKeys", {});
return computed(() => {
if (!(container.value.storageKey in storage.value)) {
// Returning a temporary ref here to avoid writing an empty array to storage
const visibleKeys = ref<string[][]>([]);
watchOnce(visibleKeys, () => (storage.value[container.value.storageKey] = visibleKeys.value), { deep: true });
return visibleKeys.value;
}
return storage.value[container.value.storageKey];
});
}
export const pinnedContainers = useProfileStorage("pinned", new Set<string>());
+45
View File
@@ -0,0 +1,45 @@
type Toast = {
id: string;
createdAt: Date;
title?: string;
message: string;
type: "success" | "error" | "warning" | "info";
};
type ToastOptions = {
expire?: number;
once?: boolean;
};
const toasts = ref<Toast[]>([]);
const showToast = (
toast: Omit<Toast, "id" | "createdAt"> & { id?: string },
{ expire = -1, once = false }: ToastOptions = { expire: -1, once: false },
) => {
if (once && toasts.value.some((t) => t.id === toast.id)) {
return;
}
toasts.value.push({
id: Date.now().toString(),
createdAt: new Date(),
...toast,
});
if (expire > 0) {
setTimeout(() => {
removeToast(toasts.value[0].id);
}, expire);
}
};
const removeToast = (id: Toast["id"]) => {
toasts.value = toasts.value.filter((toast) => toast.id !== id);
};
export const useToast = () => {
return {
toasts,
showToast,
removeToast,
};
};
@@ -1,12 +1,12 @@
import { ComplexLogEntry, type JSONObject, type LogEntry } from "@/models/LogEntry";
import type { ComputedRef, Ref } from "vue";
import type { Ref } from "vue";
export function useVisibleFilter(visibleKeys: ComputedRef<Ref<string[][]>>) {
export function useVisibleFilter(visibleKeys: Ref<string[][]>) {
function filteredPayload(messages: Ref<LogEntry<string | JSONObject>[]>) {
return computed(() => {
return messages.value.map((d) => {
if (d instanceof ComplexLogEntry) {
return ComplexLogEntry.fromLogEvent(d, visibleKeys.value);
return ComplexLogEntry.fromLogEvent(d, visibleKeys);
} else {
return d;
}
-23
View File
@@ -1,23 +0,0 @@
import { Container } from "@/models/Container";
const DOZZLE_HOST = "DOZZLE_HOST";
export const sessionHost = useSessionStorage<string | null>(DOZZLE_HOST, null);
if (config.hosts.length === 1 && !sessionHost.value) {
sessionHost.value = config.hosts[0].id;
}
export function persistentVisibleKeys(container: ComputedRef<Container>) {
return computed(() => useStorage(container.value.storageKey, []));
}
const DOZZLE_PINNED_CONTAINERS = "DOZZLE_PINNED_CONTAINERS";
export const pinnedContainers = useStorage(DOZZLE_PINNED_CONTAINERS, new Set<string>());
export function togglePinnedContainer(id: string) {
if (pinnedContainers.value.has(id)) {
pinnedContainers.value.delete(id);
} else {
pinnedContainers.value.add(id);
}
}
+59 -63
View File
@@ -1,5 +1,5 @@
<template>
<main v-if="!authorizationNeeded">
<div>
<mobile-menu v-if="isMobile" @search="showFuzzySearch"></mobile-menu>
<splitpanes @resized="onResized($event)">
<pane min-size="10" :size="menuWidth" v-if="!isMobile && !collapseNav">
@@ -7,7 +7,7 @@
</pane>
<pane min-size="10">
<splitpanes>
<pane class="has-min-height router-view">
<pane class="router-view min-h-screen">
<router-view></router-view>
</pane>
<template v-if="!isMobile">
@@ -24,35 +24,70 @@
</splitpanes>
</pane>
</splitpanes>
<button
@click="collapse"
class="button is-small is-rounded"
:class="{ collapsed: collapseNav }"
id="hide-nav"
<label
class="btn btn-circle swap btn-neutral swap-rotate fixed -left-12 bottom-4 w-16 transition-all hover:-left-4"
:class="{ '!-left-6': collapseNav }"
v-if="!isMobile"
>
<span class="icon ml-2" v-if="collapseNav">
<mdi:light-chevron-right />
</span>
<span class="icon" v-else>
<mdi:light-chevron-left />
</span>
</button>
</main>
<input type="checkbox" v-model="collapseNav" />
<mdi:chevron-right class="swap-on" />
<mdi:chevron-left class="swap-off" />
</label>
</div>
<dialog ref="modal" class="modal items-start bg-white/20 backdrop:backdrop-blur-sm" @close="open = false">
<div class="modal-box max-w-2xl bg-transparent pt-20 shadow-none">
<FuzzySearchModal @close="open = false" v-if="open" />
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<div class="toast toast-end whitespace-normal">
<div
class="alert max-w-xl"
v-for="toast in toasts"
:key="toast.id"
:class="{
'alert-error': toast.type === 'error',
'alert-info': toast.type === 'info',
'alert-warning': toast.type === 'warning',
}"
>
<carbon:information class="size-6 shrink-0 stroke-current" v-if="toast.type === 'info'" />
<carbon:warning class="size-6 shrink-0 stroke-current" v-else-if="toast.type === 'error'" />
<carbon:warning class="size-6 shrink-0 stroke-current" v-else-if="toast.type === 'warning'" />
<div>
<h3 class="text-lg font-bold" v-if="toast.title">{{ toast.title }}</h3>
<div v-html="toast.message" class="[&>a]:underline"></div>
</div>
<div>
<button class="btn btn-circle btn-xs" @click="removeToast(toast.id)"><mdi:close /></button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
// @ts-ignore - splitpanes types are not available
import { Splitpanes, Pane } from "splitpanes";
import { useProgrammatic } from "@oruga-ui/oruga-next";
import FuzzySearchModal from "@/components/FuzzySearchModal.vue";
const { oruga } = useProgrammatic();
const { authorizationNeeded } = config;
import { collapseNav } from "@/stores/settings";
const containerStore = useContainerStore();
const { activeContainers } = storeToRefs(containerStore);
const { toasts, removeToast } = useToast();
const modal = ref<HTMLDialogElement>();
const open = ref(false);
watch(open, () => {
if (open.value) {
modal.value?.showModal();
} else {
modal.value?.close();
}
});
onKeyStroke("k", (e) => {
if ((e.ctrlKey || e.metaKey) && !e.shiftKey) {
showFuzzySearch();
@@ -61,17 +96,9 @@ onKeyStroke("k", (e) => {
});
function showFuzzySearch() {
oruga.modal.open({
// parent: this,
component: FuzzySearchModal,
animation: "false",
width: 600,
active: true,
});
}
function collapse() {
collapseNav.value = !collapseNav.value;
open.value = true;
}
function onResized(e: any) {
if (e.length == 2) {
menuWidth.value = e[0].size;
@@ -79,14 +106,9 @@ function onResized(e: any) {
}
</script>
<style scoped lang="scss">
<style scoped lang="postcss">
:deep(.splitpanes--vertical > .splitpanes__splitter) {
min-width: 3px;
background: var(--border-color);
&:hover {
background: var(--border-hover-color);
}
@apply min-w-[3px] bg-base-lighter hover:bg-secondary;
}
@media screen and (max-width: 768px) {
@@ -94,30 +116,4 @@ function onResized(e: any) {
padding-top: 75px;
}
}
.button.has-no-border {
border-color: transparent !important;
}
.has-min-height {
min-height: 100vh;
}
#hide-nav {
position: fixed;
left: 10px;
bottom: 10px;
&.collapsed {
left: -40px;
width: 60px;
padding-left: 40px;
color: var(--text-strong-color);
background: var(--scheme-main);
&:hover {
left: -25px;
}
}
}
</style>
+5 -7
View File
@@ -1,9 +1,7 @@
<template>
<main>
<router-view></router-view>
</main>
<div class="hero min-h-dvh">
<div class="hero-content">
<router-view></router-view>
</div>
</div>
</template>
<script lang="ts" setup></script>
<style scoped lang="scss"></style>
+125
View File
@@ -0,0 +1,125 @@
@import "splitpanes/dist/splitpanes.css";
@define-mixin light {
--base-lighter-color: 100% 0 0;
--base-color: 97% 0 0;
--base-darker-color: 90% 0 0;
--base-content-color: 33% 0 0;
--primary-color: 79.96% 0.143 176.65;
--secondary-color: 82.67% 0.15380739746702807 77.82;
}
@define-mixin dark {
--base-lighter-color: 25% 0 0;
--base-color: 18% 0 0;
--base-darker-color: 11% 0 0;
--base-content-color: 89.23% 0 0;
--primary-color: 70.96% 0.143 176.65;
--secondary-color: 91.36% 0.11098 90.1243;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--p: var(--primary-color);
--pc: 89.23% 0;
--s: var(--secondary-color);
--sc: 14.57% 0 0;
--b1: var(--base-lighter-color);
--b2: var(--base-color);
--b3: var(--base-darker-color);
--bc: var(--base-content-color);
--in: 65% 0.171 249.5;
--inc: 100% 0 0;
--er: 64% 0.218 28.85;
--erc: 100% 0 0;
--wa: 70% 0.186 48.13;
--wac: 100% 0 0;
--n: var(--base-lighter-color);
}
html[data-theme="dark"] {
@mixin dark;
}
@media (prefers-color-scheme: dark) {
html {
@mixin dark;
}
}
html[data-theme="light"] {
@mixin light;
}
@media (prefers-color-scheme: light) {
html {
@mixin light;
}
}
@media screen and (max-device-width: 480px) {
body {
-webkit-text-size-adjust: 100%;
}
}
h1 {
@apply text-3xl;
}
h2 {
@apply text-2xl;
}
h3 {
@apply text-lg;
}
.events mark {
@apply inline-block rounded-sm bg-secondary;
animation: pops 200ms ease-out;
}
@keyframes pops {
0% {
transform: scale(1.5);
}
100% {
transform: scale(1.05);
}
}
}
@layer components {
.input {
@apply !outline-none;
&.input-ghost {
@apply focus:border-transparent;
}
}
.btn {
@apply font-normal normal-case;
}
.btn:not(.btn-primary):not(.btn-secondary) {
@apply hover:bg-base-lighter;
}
}
@layer utilities {
@media (max-width: 768px) {
.mobile-hidden {
display: none;
}
}
.active-primary {
--n: var(--p);
--nc: var(--pc);
@apply active;
}
.link-primary {
@apply underline-offset-4 hover:underline;
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import "./styles.scss";
import "./main.css";
import { createApp, App as VueApp } from "vue";
import App from "./App.vue";
+8 -2
View File
@@ -1,5 +1,11 @@
import { describe, expect, test } from "vitest";
import { describe, expect, test, vi } from "vitest";
import { Container } from "./Container";
vi.mock("@/stores/config", () => ({
__esModule: true,
default: { base: "", hosts: [{ name: "localhost", id: "localhost" }] },
}));
describe("Container", () => {
const names = [
[
@@ -12,7 +18,7 @@ describe("Container", () => {
];
test.each(names)("name %s should be %s and %s", (name, expectedName, expectedSwarmId) => {
const c = new Container("id", new Date(), "image", name!, "command", "host", "status", "created");
const c = new Container("id", new Date(), "image", name!, "command", "host", {}, "status", "created", []);
expect(c.name).toBe(expectedName);
expect(c.swarmId).toBe(expectedSwarmId);
});
+21 -6
View File
@@ -1,15 +1,24 @@
import type { ContainerHealth, ContainerStat, ContainerState } from "@/types/Container";
import type { UseThrottledRefHistoryReturn } from "@vueuse/core";
import { useExponentialMovingAverage } from "@/utils";
import { useExponentialMovingAverage, useSimpleRefHistory } from "@/utils";
import { Ref } from "vue";
type Stat = Omit<ContainerStat, "id">;
const SWARM_ID_REGEX = /(\.[a-z0-9]{25})+$/i;
const hosts = computed(() =>
config.hosts.reduce(
(acc, item) => {
acc[item.id] = item;
return acc;
},
{} as Record<string, { name: string; id: string }>,
),
);
export class Container {
private _stat: Ref<Stat>;
private readonly throttledStatHistory: UseThrottledRefHistoryReturn<Stat, Stat>;
private readonly _statsHistory: Ref<Stat[]>;
public readonly swarmId: string | null = null;
public readonly isSwarm: boolean = false;
private readonly movingAverageStat: Ref<Stat>;
@@ -21,12 +30,14 @@ export class Container {
public readonly name: string,
public readonly command: string,
public readonly host: string,
public readonly labels = {} as Record<string, string>,
public status: string,
public state: ContainerState,
stats: Stat[],
public health?: ContainerHealth,
) {
this._stat = ref({ cpu: 0, memory: 0, memoryUsage: 0 });
this.throttledStatHistory = useThrottledRefHistory(this._stat, { capacity: 300, deep: true, throttle: 1000 });
this._statsHistory = useSimpleRefHistory(this._stat, { capacity: 300, deep: true, initial: stats });
this.movingAverageStat = useExponentialMovingAverage(this._stat, 0.2);
const match = name.match(SWARM_ID_REGEX);
@@ -37,8 +48,8 @@ export class Container {
}
}
get statHistory() {
return unref(this.throttledStatHistory.history);
get statsHistory() {
return unref(this._statsHistory);
}
get movingAverage() {
@@ -49,6 +60,10 @@ export class Container {
return unref(this._stat);
}
get hostLabel() {
return hosts.value[this.host]?.name;
}
get storageKey() {
return `${stripVersion(this.image)}:${this.command}`;
}
+16 -19
View File
@@ -5,31 +5,28 @@ import SimpleLogItem from "@/components/LogViewer/SimpleLogItem.vue";
import DockerEventLogItem from "@/components/LogViewer/DockerEventLogItem.vue";
import SkippedEntriesLogItem from "@/components/LogViewer/SkippedEntriesLogItem.vue";
export interface HasComponent {
getComponent(): Component;
}
export type JSONValue = string | number | boolean | JSONObject | Array<JSONValue>;
export type JSONObject = { [x: string]: JSONValue };
export type Position = "start" | "end" | "middle" | undefined;
export type Std = "stdout" | "stderr";
export type Level = "debug" | "info" | "warn" | "error" | "fatal" | "trace" | "unknown";
export interface LogEvent {
readonly m: string | JSONObject;
readonly ts: number;
readonly id: number;
readonly l: string;
readonly l: Level;
readonly p: Position;
readonly s: "stdout" | "stderr" | "unknown";
}
export abstract class LogEntry<T extends string | JSONObject> implements HasComponent {
export abstract class LogEntry<T extends string | JSONObject> {
protected readonly _message: T;
constructor(
message: T,
public readonly id: number,
public readonly date: Date,
public readonly std: Std,
public readonly level?: string,
public readonly level?: Level,
) {
this._message = message;
}
@@ -46,7 +43,7 @@ export class SimpleLogEntry extends LogEntry<string> {
message: string,
id: number,
date: Date,
public readonly level: string,
public readonly level: Level,
public readonly position: Position,
public readonly std: Std,
) {
@@ -64,7 +61,7 @@ export class ComplexLogEntry extends LogEntry<JSONObject> {
message: JSONObject,
id: number,
date: Date,
public readonly level: string,
public readonly level: Level,
public readonly std: Std,
visibleKeys?: Ref<string[][]>,
) {
@@ -102,7 +99,7 @@ export class DockerEventLogEntry extends LogEntry<string> {
constructor(
message: string,
date: Date,
public readonly event: string,
public readonly event: "container-stopped" | "container-started",
) {
super(message, date.getTime(), date, "stderr", "info");
}
@@ -148,7 +145,15 @@ export class SkippedLogsEntry extends LogEntry<string> {
}
export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
if (typeof event.m === "string") {
if (isObject(event.m)) {
return new ComplexLogEntry(
event.m,
event.id,
new Date(event.ts),
event.l,
event.s === "unknown" ? "stderr" : event.s ?? "stderr",
);
} else {
return new SimpleLogEntry(
event.m,
event.id,
@@ -157,13 +162,5 @@ export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
event.p,
event.s === "unknown" ? "stderr" : event.s ?? "stderr",
);
} else {
return new ComplexLogEntry(
event.m,
event.id,
new Date(event.ts),
event.l,
event.s === "unknown" ? "stderr" : event.s ?? "stderr",
);
}
}
+20 -17
View File
@@ -1,20 +1,23 @@
import { type App } from "vue";
import { createI18n } from "vue-i18n";
import { locale } from "@/stores/settings";
export const install = (app: App) => {
const messages = Object.fromEntries(
Object.entries(import.meta.glob<{ default: any }>("../../locales/*.y(a)?ml", { eager: true })).map(
([key, value]) => {
const yaml = key.endsWith(".yaml");
return [key.slice(14, yaml ? -5 : -4), value.default];
},
),
);
const i18n = createI18n({
legacy: false,
locale: navigator.language.slice(0, 2),
fallbackLocale: "en",
messages,
});
app.use(i18n);
};
import messages from "@intlify/unplugin-vue-i18n/messages";
const defaultLocale = messages?.hasOwnProperty(navigator.language)
? navigator.language
: navigator.language.slice(0, 2);
const i18n = createI18n({
legacy: false,
locale: locale.value || defaultLocale,
fallbackLocale: "en",
messages,
});
watch(locale, (value) => {
i18n.global.locale.value = value || defaultLocale;
});
export const install = (app: App) => app.use(i18n);
export default i18n;
-16
View File
@@ -1,16 +0,0 @@
import { type App } from "vue";
import { Autocomplete, Button, Dropdown, Switch, Skeleton, Field, Table, Modal, Config } from "@oruga-ui/oruga-next";
import { bulmaConfig } from "@oruga-ui/theme-bulma";
export const install = (app: App) => {
app
.use(Autocomplete)
.use(Button)
.use(Dropdown)
.use(Switch)
.use(Modal)
.use(Field)
.use(Skeleton)
.use(Table)
.use(Config, bulmaConfig);
};
+6 -8
View File
@@ -2,15 +2,13 @@ import { type App } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import pages from "~pages";
import { setupLayouts } from "virtual:generated-layouts";
import config from "@/stores/config";
const routes = setupLayouts(pages);
export const router = createRouter({
history: createWebHistory(withBase("/")),
routes,
});
export const install = (app: App) => {
const routes = setupLayouts(pages);
const router = createRouter({
history: createWebHistory(`${config.base}/`),
routes,
});
app.use(router);
};
+7 -8
View File
@@ -1,14 +1,13 @@
<template>
<div class="hero is-halfheight">
<div class="hero-body">
<div class="container has-text-centered">
<h1 class="title">
404.
<small class="subtitle">{{ $t("error.page-not-found") }}</small>
</h1>
<page-with-links>
<div class="hero min-h-screen bg-base-200">
<div class="hero-content text-center">
<div class="max-w-md">
<p class="py-6 text-2xl font-bold">{{ $t("error.page-not-found") }}</p>
</div>
</div>
</div>
</div>
</page-with-links>
</template>
<script lang="ts" setup>
+12 -5
View File
@@ -1,10 +1,17 @@
<template>
<search></search>
<log-container :id="id" show-title :scrollable="activeContainers.length > 0" v-if="currentContainer"></log-container>
<div v-else-if="ready" class="notification is-warning is-light m-6">
<h1 class="title">
{{ $t("error.container-not-found") }}
</h1>
<log-container
:id="id"
:show-title="true"
:scrollable="activeContainers.length > 0"
v-if="currentContainer"
></log-container>
<div v-else-if="ready" class="hero min-h-screen bg-base-200">
<div class="hero-content text-center">
<div class="max-w-md">
<p class="py-6 text-2xl font-bold">{{ $t("error.container-not-found") }}</p>
</div>
</div>
</div>
</template>
+36 -91
View File
@@ -1,52 +1,31 @@
<template>
<div class="section tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child box">
<div class="level-item has-text-centered">
<div>
<p class="title">{{ runningContainers.length }} / {{ containers.length }}</p>
<p class="heading">{{ $t("label.running") }} / {{ $t("label.total-containers") }}</p>
</div>
<page-with-links class="gap-16">
<section>
<div class="stats grid bg-base-lighter shadow">
<div class="stat">
<div class="stat-value">{{ runningContainers.length }} / {{ containers.length }}</div>
<div class="stat-title">{{ $t("label.running") }} / {{ $t("label.total-containers") }}</div>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child box">
<div class="level-item has-text-centered">
<div>
<p class="title">{{ totalCpu }}%</p>
<p class="heading">{{ $t("label.total-cpu-usage") }}</p>
</div>
<div class="stat">
<div class="stat-value">{{ totalCpu.toFixed(0) }}%</div>
<div class="stat-title">{{ $t("label.total-cpu-usage") }}</div>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child box">
<div class="level-item has-text-centered">
<div>
<p class="title">{{ formatBytes(totalMem) }}</p>
<p class="heading">{{ $t("label.total-mem-usage") }}</p>
</div>
<div class="stat">
<div class="stat-value">{{ formatBytes(totalMem) }}</div>
<div class="stat-title">{{ $t("label.total-mem-usage") }}</div>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child box">
<div class="level-item has-text-centered">
<div>
<p class="title">{{ version }}</p>
<p class="heading">{{ $t("label.dozzle-version") }}</p>
</div>
</div>
</div>
</div>
</div>
<section class="section table-container">
<div class="box">
<div class="stat">
<div class="stat-value">{{ version }}</div>
<div class="stat-title">{{ $t("label.dozzle-version") }}</div>
</div>
</div>
</section>
<section>
<container-table :containers="runningContainers"></container-table>
</div>
</section>
</section>
</page-with-links>
</template>
<script lang="ts" setup>
@@ -60,7 +39,7 @@ const { containers, ready } = storeToRefs(containerStore) as unknown as {
ready: Ref<boolean>;
};
const mostRecentContainers = $computed(() => [...containers.value].sort((a, b) => +b.created - +a.created));
const mostRecentContainers = $computed(() => containers.value.toSorted((a, b) => +b.created - +a.created));
const runningContainers = $computed(() => mostRecentContainers.filter((c) => c.state === "running"));
let totalCpu = $ref(0);
@@ -87,58 +66,24 @@ watchEffect(() => {
}
});
</script>
<style lang="scss" scoped>
.panel {
border: 1px solid var(--border-color);
.panel-block,
.panel-tabs {
border-color: var(--border-color);
.is-active {
border-color: var(--border-hover-color);
}
.name {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.status {
margin-left: auto;
white-space: nowrap;
}
}
}
@media screen and (max-width: 768px) {
.pb-0-is-mobile {
padding-bottom: 0 !important;
}
.pt-0-is-mobile {
padding-top: 0 !important;
}
}
.icon {
padding: 10px 3px;
}
.bar-chart {
height: 1.5em;
.bar-text {
font-size: 0.9em;
padding: 0 0.5em;
}
}
<style lang="postcss" scoped>
:deep(tr td) {
padding-top: 1em;
padding-bottom: 1em;
}
.stat > div {
@apply text-center;
}
.stat-value {
@apply font-light;
}
.stat-title {
@apply font-light;
}
.section + .section {
padding-top: 0;
}
+39 -49
View File
@@ -1,50 +1,39 @@
<template>
<div class="hero is-halfheight">
<div class="hero-body">
<div class="container">
<section class="columns is-centered section">
<div class="column is-4">
<div class="card">
<div class="card-content">
<form action="" method="post" @submit.prevent="onLogin" ref="form">
<div class="field">
<label class="label">{{ $t("label.username") }}</label>
<div class="control">
<input
class="input"
type="text"
name="username"
autocomplete="username"
v-model="username"
autofocus
/>
</div>
</div>
<div class="field">
<label class="label">{{ $t("label.password") }}</label>
<div class="control">
<input
class="input"
type="password"
name="password"
autocomplete="current-password"
v-model="password"
/>
</div>
<p class="help is-danger" v-if="error">{{ $t("error.invalid-auth") }}</p>
</div>
<div class="field is-grouped is-grouped-centered mt-5">
<p class="control">
<button class="button is-primary" type="submit">{{ $t("button.login") }}</button>
</p>
</div>
</form>
</div>
</div>
</div>
</section>
</div>
<div class="card w-96 flex-shrink-0 bg-base-lighter shadow-2xl">
<div class="card-body">
<form action="" method="post" @submit.prevent="onLogin" ref="form">
<div class="form-control">
<label class="label">
<span class="label-text"> {{ $t("label.username") }} </span>
</label>
<input
class="input input-bordered"
type="text"
name="username"
autocomplete="username"
v-model="username"
autofocus
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Password</span>
</label>
<input
class="input input-bordered"
type="password"
name="password"
autocomplete="current-password"
v-model="password"
/>
</div>
<label class="label text-red" v-if="error">
{{ $t("error.invalid-auth") }}
</label>
<div class="form-control mt-6">
<button class="btn btn-primary" type="submit">{{ $t("button.login") }}</button>
</div>
</form>
</div>
</div>
</template>
@@ -60,14 +49,15 @@ let password = $ref("");
let form: HTMLFormElement | undefined = $ref();
async function onLogin() {
const response = await fetch(`${config.base}/api/validateCredentials`, {
const url = config.authProvider === "simple" ? "/api/token" : "/api/validateCredentials";
const response = await fetch(withBase(url), {
body: new FormData(form),
method: "post",
method: "POST",
});
if (response.status == 200) {
error = false;
window.location.href = `${config.base}/`;
window.location.href = withBase("/");
} else {
error = true;
}
+171 -171
View File
@@ -1,208 +1,208 @@
<template>
<div>
<section class="section">
<page-with-links>
<section>
<div class="has-underline">
<h2 class="title is-4">{{ $t("settings.about") }}</h2>
<h2>{{ $t("settings.about") }}</h2>
</div>
<div>
<span v-html="$t('settings.using-version', { version: currentVersion })"></span>
<span v-html="$t('settings.using-version', { version: config.version })"></span>
<div
v-if="hasUpdate"
v-html="$t('settings.update-available', { nextVersion: nextRelease.name, href: nextRelease.html_url })"
v-html="$t('settings.update-available', { nextVersion: latest?.name, href: latest?.htmlUrl })"
></div>
</div>
</section>
<section class="section">
<section class="flex flex-col @container">
<div class="has-underline">
<h2 class="title is-4">{{ $t("settings.display") }}</h2>
<h2>{{ $t("settings.display") }}</h2>
</div>
<div class="item">
<o-switch v-model="smallerScrollbars"> {{ $t("settings.small-scrollbars") }} </o-switch>
</div>
<div class="item">
<o-switch v-model="showTimestamp"> {{ $t("settings.show-timesamps") }} </o-switch>
</div>
<div class="item">
<o-switch v-model="showStd"> {{ $t("settings.show-std") }} </o-switch>
</div>
<section class="grid-cols-2 gap-4 @3xl:grid">
<div class="flex flex-col gap-2 text-balance @3xl:pr-8">
<toggle v-model="compact"> {{ $t("settings.compact") }} </toggle>
<div class="item">
<o-switch v-model="softWrap"> {{ $t("settings.soft-wrap") }}</o-switch>
</div>
<toggle v-model="smallerScrollbars"> {{ $t("settings.small-scrollbars") }} </toggle>
<div class="item">
<div class="columns is-vcentered">
<div class="column is-narrow">
<o-field>
<o-dropdown v-model="hourStyle" aria-role="list">
<template #trigger>
<o-button variant="primary" type="button">
<span class="is-capitalized">{{ hourStyle }}</span>
<span class="icon">
<carbon:caret-down />
</span>
</o-button>
</template>
<toggle v-model="showTimestamp">{{ $t("settings.show-timesamps") }}</toggle>
<o-dropdown-item :value="value" aria-role="listitem" v-for="value in ['auto', '12', '24']" :key="value">
<span class="is-capitalized">{{ value }}</span>
</o-dropdown-item>
</o-dropdown>
</o-field>
</div>
<div class="column">
{{ $t("settings.12-24-format") }}
</div>
<toggle v-model="showStd">{{ $t("settings.show-std") }}</toggle>
<toggle v-model="softWrap">{{ $t("settings.soft-wrap") }}</toggle>
<labeled-input>
<template #label>
{{ $t("settings.locale") }}
</template>
<template #input>
<dropdown-menu
v-model="locale"
:options="[
{ label: 'Auto', value: '' },
...availableLocales.map((l) => ({ label: l.toLocaleUpperCase(), value: l })),
]"
/>
</template>
</labeled-input>
<labeled-input>
<template #label>
{{ $t("settings.datetime-format") }}
</template>
<template #input>
<div class="flex gap-2">
<dropdown-menu
v-model="dateLocale"
:options="[
{ label: 'Auto', value: 'auto' },
{ label: 'MM/DD/YYYY', value: 'en-US' },
{ label: 'DD/MM/YYYY', value: 'en-GB' },
{ label: 'DD.MM.YYYY', value: 'de-DE' },
{ label: 'YYYY-MM-DD', value: 'en-CA' },
]"
/>
<dropdown-menu
v-model="hourStyle"
:options="[
{ label: 'Auto', value: 'auto' },
{ label: '12', value: '12' },
{ label: '24', value: '24' },
]"
/>
</div>
</template>
</labeled-input>
<labeled-input>
<template #label>
{{ $t("settings.font-size") }}
</template>
<template #input>
<dropdown-menu
v-model="size"
:options="[
{ label: 'Small', value: 'small' },
{ label: 'Medium', value: 'medium' },
{ label: 'Large', value: 'large' },
]"
/>
</template>
</labeled-input>
<labeled-input>
<template #label>
{{ $t("settings.color-scheme") }}
</template>
<template #input>
<dropdown-menu
v-model="lightTheme"
:options="[
{ label: 'Auto', value: 'auto' },
{ label: 'Dark', value: 'dark' },
{ label: 'Light', value: 'light' },
]"
/>
</template>
</labeled-input>
</div>
</div>
<div class="item">
<div class="columns is-vcentered">
<div class="column is-narrow">
<o-field>
<o-dropdown v-model="size" aria-role="list">
<template #trigger>
<o-button variant="primary" type="button">
<span class="is-capitalized">{{ size }}</span>
<span class="icon">
<carbon:caret-down />
</span>
</o-button>
</template>
<log-viewer
:messages="fakeMessages"
:visible-keys="keys"
:last-selected-item="undefined"
class="hidden overflow-hidden rounded-lg border border-base-content/50 shadow @3xl:block"
/>
</section>
</section>
<o-dropdown-item
:value="value"
aria-role="listitem"
v-for="value in ['small', 'medium', 'large']"
:key="value"
>
<span class="is-capitalized">{{ value }}</span>
</o-dropdown-item>
</o-dropdown>
</o-field>
</div>
<div class="column">{{ $t("settings.font-size") }}</div>
</div>
<section class="flex flex-col gap-2">
<div class="has-underline">
<h2>{{ $t("settings.options") }}</h2>
</div>
<div>
<toggle v-model="search">
{{ $t("settings.search") }} <key-shortcut char="f" class="align-top"></key-shortcut>
</toggle>
</div>
<div class="item">
<div class="columns is-vcentered">
<div class="column is-narrow">
<o-field>
<o-dropdown v-model="lightTheme" aria-role="list">
<template #trigger>
<o-button variant="primary" type="button">
<span class="is-capitalized">{{ lightTheme }}</span>
<span class="icon">
<carbon:caret-down />
</span>
</o-button>
</template>
<o-dropdown-item
:value="value"
aria-role="listitem"
v-for="value in ['auto', 'dark', 'light']"
:key="value"
>
<span class="is-capitalized">{{ value }}</span>
</o-dropdown-item>
</o-dropdown>
</o-field>
</div>
<div class="column">{{ $t("settings.color-scheme") }}</div>
</div>
<div>
<toggle v-model="showAllContainers">{{ $t("settings.show-stopped-containers") }}</toggle>
</div>
<div>
<toggle v-model="automaticRedirect">{{ $t("settings.automatic-redirect") }}</toggle>
</div>
</section>
<section class="section">
<div class="has-underline">
<h2 class="title is-4">{{ $t("settings.options") }}</h2>
</div>
<div class="item">
<o-switch v-model="search">
<span>{{ $t("settings.search") }} <key-shortcut char="f"></key-shortcut></span>
</o-switch>
</div>
<div class="item">
<o-switch v-model="showAllContainers"> {{ $t("settings.show-stopped-containers") }} </o-switch>
</div>
</section>
</div>
</page-with-links>
</template>
<script lang="ts" setup>
import {
search,
lightTheme,
smallerScrollbars,
showTimestamp,
showStd,
hourStyle,
showAllContainers,
size,
softWrap,
} from "@/composables/settings";
import { ComplexLogEntry, SimpleLogEntry } from "@/models/LogEntry";
const { t } = useI18n();
import {
automaticRedirect,
compact,
hourStyle,
dateLocale,
lightTheme,
search,
showAllContainers,
showStd,
showTimestamp,
size,
smallerScrollbars,
softWrap,
locale,
} from "@/stores/settings";
const { t, availableLocales } = useI18n();
setTitle(t("title.settings"));
const { latest, hasUpdate } = useReleases();
const currentVersion = config.version;
let nextRelease = $ref({ html_url: "", name: "" });
let hasUpdate = $ref(false);
const keys = ref<string[][]>([]);
const hoursAgo = (hours: number) => {
const date = new Date();
date.setHours(date.getHours() - hours);
return date;
};
async function fetchNextRelease() {
if (!["dev", "master"].includes(currentVersion)) {
const response = await fetch("https://api.github.com/repos/amir20/dozzle/releases/latest");
if (response.ok) {
const release = await response.json();
hasUpdate =
release.tag_name.slice(1).localeCompare(currentVersion, undefined, { numeric: true, sensitivity: "base" }) > 0;
nextRelease = release;
}
} else {
hasUpdate = true;
nextRelease = {
html_url: "",
name: "master",
};
}
}
fetchNextRelease();
const fakeMessages = [
new SimpleLogEntry("This is a preview of the logs", 1, hoursAgo(16), "info", undefined, "stdout"),
new SimpleLogEntry("A warning log looks like this", 2, hoursAgo(12), "warn", undefined, "stdout"),
new SimpleLogEntry("This is a multi line error message", 3, hoursAgo(7), "error", "start", "stderr"),
new SimpleLogEntry("with a second line", 4, hoursAgo(2), "error", "middle", "stderr"),
new SimpleLogEntry("and finally third line.", 5, new Date(), "error", "end", "stderr"),
new ComplexLogEntry(
{
message: "This is a complex log entry as json",
context: {
key: "value",
key2: "value2",
},
},
6,
new Date(),
"info",
"stdout",
keys,
),
new SimpleLogEntry(
"This is a very very long message which would wrap by default. Disabling soft wraps would disable this. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ",
7,
new Date(),
"debug",
undefined,
"stderr",
),
];
</script>
<style lang="scss" scoped>
.title {
color: var(--title-color);
}
a.next-release {
text-decoration: underline;
&:hover {
text-decoration: none;
}
}
.section {
padding: 1rem 1.5rem;
}
<style lang="postcss" scoped>
.has-underline {
border-bottom: 1px solid var(--border-color);
padding: 1em 0px;
margin-bottom: 1em;
@apply mb-4 border-b border-base-content/50 py-4;
}
.item {
padding: 1em 0;
}
code {
border-radius: 4px;
background-color: #444;
:deep(a:not(.menu a)) {
@apply text-primary underline-offset-4 hover:underline;
}
</style>
+21 -4
View File
@@ -1,13 +1,28 @@
import { type Settings } from "@/stores/settings";
const text = document.querySelector("script#config__json")?.textContent || "{}";
interface Config {
export interface Config {
version: string;
base: string;
authorizationNeeded: boolean;
secured: boolean;
maxLogs: number;
hostname: string;
hosts: { name: string; id: string }[];
authProvider: "simple" | "none" | "forward-proxy";
enableActions: boolean;
user?: {
username: string;
email: string;
name: string;
};
profile?: Profile;
}
export interface Profile {
settings?: Settings;
pinned?: Set<string>;
visibleKeys?: { [key: string]: string[][] };
releaseSeen?: string;
}
const pageConfig = JSON.parse(text);
@@ -19,4 +34,6 @@ const config: Config = {
config.version = config.version.replace(/^v/, "");
export default config;
export default Object.freeze(config);
export const withBase = (path: string) => `${config.base}${path}`;
+46 -6
View File
@@ -2,6 +2,11 @@ import { acceptHMRUpdate, defineStore } from "pinia";
import { Ref, UnwrapNestedRefs } from "vue";
import type { ContainerHealth, ContainerJson, ContainerStat } from "@/types/Container";
import { Container } from "@/models/Container";
import i18n from "@/modules/i18n";
const { showToast } = useToast();
// @ts-ignore
const { t } = i18n.global;
export const useContainerStore = defineStore("container", () => {
const containers: Ref<Container[]> = ref([]);
@@ -29,7 +34,20 @@ export const useContainerStore = defineStore("container", () => {
function connect() {
es?.close();
ready.value = false;
es = new EventSource(`${config.base}/api/events/stream`);
es = new EventSource(withBase("/api/events/stream"));
es.addEventListener("error", (e) => {
if (es?.readyState === EventSource.CLOSED) {
showToast(
{
id: "events-stream",
message: t("error.events-stream.message"),
title: t("error.events-stream.title"),
type: "error",
},
{ once: true },
);
}
});
es.addEventListener("containers-changed", (e: Event) =>
updateContainers(JSON.parse((e as MessageEvent).data) as ContainerJson[]),
@@ -46,7 +64,7 @@ export const useContainerStore = defineStore("container", () => {
const event = JSON.parse((e as MessageEvent).data) as { actorId: string };
const container = allContainersById.value[event.actorId];
if (container) {
container.state = "dead";
container.state = "exited";
}
});
@@ -58,11 +76,33 @@ export const useContainerStore = defineStore("container", () => {
}
});
es.onopen = () => {
if (containers.value.length > 0) {
containers.value = [];
}
};
watchOnce(containers, () => (ready.value = true));
}
connect();
(async function () {
try {
await until(ready).toBe(true, { timeout: 8000, throwOnTimeout: true });
} catch (e) {
showToast(
{
id: "events-timeout",
message: t("error.events-timeout.message"),
title: t("error.events-timeout.title"),
type: "error",
},
{ once: true },
);
}
})();
const updateContainers = (containersPayload: ContainerJson[]) => {
const existingContainers = containersPayload.filter((c) => allContainersById.value[c.id]);
const newContainers = containersPayload.filter((c) => !allContainersById.value[c.id]);
@@ -84,8 +124,10 @@ export const useContainerStore = defineStore("container", () => {
c.name,
c.command,
c.host,
c.labels,
c.status,
c.state,
c.stats,
c.health,
);
}),
@@ -93,8 +135,8 @@ export const useContainerStore = defineStore("container", () => {
};
const currentContainer = (id: Ref<string>) => computed(() => allContainersById.value[id.value]);
const appendActiveContainer = ({ id }: Container) => activeContainerIds.value.push(id);
const removeActiveContainer = ({ id }: Container) =>
const appendActiveContainer = ({ id }: { id: string }) => activeContainerIds.value.push(id);
const removeActiveContainer = ({ id }: { id: string }) =>
activeContainerIds.value.splice(activeContainerIds.value.indexOf(id), 1);
return {
@@ -110,8 +152,6 @@ export const useContainerStore = defineStore("container", () => {
};
});
// @ts-ignore
if (import.meta.hot) {
// @ts-ignore
import.meta.hot.accept(acceptHMRUpdate(useContainerStore, import.meta.hot));
}
+29
View File
@@ -0,0 +1,29 @@
const { data: releases } = useFetch(withBase("/api/releases")).get().json<
{
name: string;
mentionsCount: number;
createdAt: string;
body: string;
tag: string;
htmlUrl: string;
latest: boolean;
features: number;
bugFixes: number;
breaking: number;
}[]
>();
const hasUpdate = computed(() => {
if (!releases.value?.length) return false;
return releases.value[0].tag !== config.version;
});
const latest = computed(() => releases.value?.find((release) => release.latest));
export function useReleases() {
return {
hasUpdate,
latest,
releases,
};
}
@@ -1,9 +1,9 @@
import { toRefs } from "@vueuse/core";
const DOZZLE_SETTINGS_KEY = "DOZZLE_SETTINGS";
export const DEFAULT_SETTINGS: {
export type Settings = {
search: boolean;
size: "small" | "medium" | "large";
compact: boolean;
menuWidth: number;
smallerScrollbars: boolean;
showTimestamp: boolean;
@@ -11,10 +11,15 @@ export const DEFAULT_SETTINGS: {
showAllContainers: boolean;
lightTheme: "auto" | "dark" | "light";
hourStyle: "auto" | "24" | "12";
dateLocale: "auto" | "en-US" | "en-GB" | "de-DE" | "en-CA";
softWrap: boolean;
collapseNav: boolean;
} = {
automaticRedirect: boolean;
locale: string;
};
export const DEFAULT_SETTINGS: Settings = {
search: true,
compact: false,
size: "medium",
menuWidth: 15,
smallerScrollbars: false,
@@ -23,17 +28,21 @@ export const DEFAULT_SETTINGS: {
showAllContainers: false,
lightTheme: "auto",
hourStyle: "auto",
dateLocale: "auto",
softWrap: true,
collapseNav: false,
automaticRedirect: true,
locale: "",
};
export const settings = useStorage(DOZZLE_SETTINGS_KEY, DEFAULT_SETTINGS);
settings.value = { ...DEFAULT_SETTINGS, ...settings.value };
export const settings = useProfileStorage("settings", DEFAULT_SETTINGS);
export const {
collapseNav,
compact,
softWrap,
hourStyle,
dateLocale,
lightTheme,
showAllContainers,
showTimestamp,
@@ -42,4 +51,6 @@ export const {
menuWidth,
size,
search,
locale,
automaticRedirect,
} = toRefs(settings);

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