Compare commits

...

473 Commits

Author SHA1 Message Date
Amir Raminfar e22d54cf74 chore: release v8.7.2 2024-11-02 10:47:03 -07:00
renovate[bot] 7bf331cf6e fix(deps): update all non-major dependencies (#3369)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-02 17:46:01 +00:00
Amir Raminfar d529ed7855 feat: checks level with equal sign (#3368) 2024-11-02 10:40:49 -07:00
Amir Raminfar 46d124b11a chore: updates version 2024-11-01 16:39:22 -07:00
Amir Raminfar d09aa5d481 chore: updates log 2024-11-01 14:17:24 -07:00
Amir Raminfar cc77c16f92 feat: skips healthcheck for agents (#3366) 2024-11-01 08:28:10 -07:00
renovate[bot] 63be3bf2d8 fix(deps): update all non-major dependencies (#3363)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-30 21:53:50 +00:00
Amir Raminfar ec29c78052 chore: updates to node v23 (#3362) 2024-10-30 08:57:06 -07:00
renovate[bot] df3f04dd50 chore(deps): update dependency @apache-arrow/esnext-esm to v18 (#3361)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-30 07:55:41 -07:00
renovate[bot] 7af7956b7f chore(deps): update all non-major dependencies (#3360)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-30 14:53:33 +00:00
renovate[bot] d21473fe0b chore(deps): update all non-major dependencies (#3359)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 17:00:09 +00:00
Jérémy Touati 4093512497 fix: fix link color when using colorized logs (#3357) 2024-10-28 12:36:45 -07:00
Amir Raminfar 0ef6252158 chore: release v8.7.1 2024-10-28 09:51:24 -07:00
Amir Raminfar 5947338966 chore: fixes vue-ignore 2024-10-28 09:37:48 -07:00
renovate[bot] db5e760ae3 chore(deps): update all non-major dependencies (#3351)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-28 15:32:26 +00:00
Amir Raminfar 1ab4464f9a chore: cleans up types (#3356) 2024-10-28 15:27:31 +00:00
renovate[bot] 45ea4ac095 chore(config): migrate renovate config (#3354)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-28 14:40:33 +00:00
Amir Raminfar c6339effd1 fix: fix base ANSI convertor colors (#3353)
Co-authored-by: Jérémy Touati <sielver@gmail.com>
2024-10-27 20:37:02 +00:00
Amir Raminfar 4e217f4e3a chore: release v8.7.0 2024-10-27 08:18:51 -07:00
Amir Raminfar c58f3b4254 chore: reverts changes to proto 2024-10-27 08:15:22 -07:00
Amir Raminfar a7680940ab chore: removes drop shadow for icons (#3350) 2024-10-27 14:21:53 +00:00
Amir Raminfar cce2e8a8b5 fix: fixes toggle all when fields have not been set for JSON (#3349) 2024-10-27 14:16:20 +00:00
Amir Raminfar 8513824fb4 chore: updates protos 2024-10-27 07:04:18 -07:00
renovate[bot] 00da15c2fc fix(deps): update all non-major dependencies (#3348)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-26 15:20:11 +00:00
Amir Raminfar c1d31313b2 fix: unfound fields are shown by default instead of hidden (#3342) 2024-10-26 15:15:29 +00:00
Amir Raminfar 6f7c1d8ba6 chore: updates snapshots 2024-10-25 16:06:50 -07:00
Amir Raminfar f1982c094d chore: fixes logo on mobile 2024-10-25 15:54:08 -07:00
yubiuser cd63b5ded8 fix: limit default SQL log entries to 100 (#3346)
Signed-off-by: yubiuser <github@yubiuser.dev>
2024-10-25 21:08:12 +00:00
Amir Raminfar 96fbee9ece feat: updates logo (#3340) 2024-10-25 17:55:23 +00:00
Amir Raminfar 36a5d7e6b9 fix: fixes timeout flag (#3345) 2024-10-25 17:48:54 +00:00
renovate[bot] 520d82246f chore(deps): update all non-major dependencies (#3343)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-25 16:21:34 +00:00
Amir Raminfar ddfd00aaed chore: release v8.6.2 2024-10-21 13:15:30 -07:00
Amir Raminfar f3ab541ad1 fix: fixes an edge case where a container might get stuck in created state (#3338) 2024-10-21 13:14:50 -07:00
renovate[bot] 4026e927f5 fix(deps): update all non-major dependencies (#3336)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 01:27:15 +00:00
Amir Raminfar b1643d490c chore: fixes typescript error 2024-10-18 10:06:50 -07:00
Amir Raminfar eab380b70d chore: fixes typescript error 2024-10-18 09:49:36 -07:00
renovate[bot] 070f1c7bdb chore(deps): update all non-major dependencies (#3335)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-18 15:59:53 +00:00
Amir Raminfar 8b1adb51c8 feat: removes rounded corners in compact mode (#3332) 2024-10-16 18:50:50 -07:00
renovate[bot] 46465356ab chore(deps): update all non-major dependencies (#3333)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-16 14:44:22 +00:00
renovate[bot] ce096983ca fix(deps): update all non-major dependencies (#3330)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 15:14:00 +00:00
Amir Raminfar ddd3a80ddb chore: updates renovate 2024-10-14 08:09:24 -07:00
Amir Raminfar aeb1fd96e2 chore: release v8.6.1 2024-10-13 08:58:18 -07:00
renovate[bot] dc9f402a1d fix(deps): update all non-major dependencies (#3327)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-13 15:55:21 +00:00
Amir Raminfar 2c2e3d5790 fix: fixes empty blank links to not show up as json (#3324) 2024-10-12 08:52:56 -07:00
Amir Raminfar 5e33094517 feat: adds a toggle for all levels (#3321) 2024-10-11 18:53:19 +00:00
renovate[bot] 853a5d4f0d fix(deps): update all non-major dependencies (#3322)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-11 18:46:49 +00:00
Amir Raminfar 8ea5876615 chore: clean up 2024-10-10 08:17:40 -07:00
Amir Raminfar c4aacb211f chore: uses path instead 2024-10-09 21:05:54 -07:00
renovate[bot] 8cf0912af0 chore(deps): update all non-major dependencies (#3320)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-09 22:42:14 +00:00
Amir Raminfar cedd112f6b chore: updates duckdb 2024-10-08 18:39:27 -07:00
Amir Raminfar 01cf239569 chore: updates grpc (#3319) 2024-10-08 19:01:19 +00:00
Amir Raminfar f4f14b900c chore: fixes typescript error 2024-10-08 11:56:38 -07:00
renovate[bot] 6722a0129e fix(deps): update all non-major dependencies (#3318)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-08 11:56:33 -07:00
Amir Raminfar 277beb940b chore: clean up home page 2024-10-08 11:49:12 -07:00
Amir Raminfar 2d62257a33 chore: fixes typecheck for arrow (#3317) 2024-10-08 16:39:52 +00:00
Amir Raminfar 5a890cbb94 chore: release v8.6.0 2024-10-06 08:03:20 -07:00
renovate[bot] 39b3b1b59d fix(deps): update all non-major dependencies (#3315)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-06 14:59:08 +00:00
Amir Raminfar 567337649e feat: supports auth ttl to override session with DOZZLE_AUTH_TTL env. (#3313) 2024-10-06 06:31:40 -07:00
Amir Raminfar 423e4e11a3 chore: cleans up urls for levels (#3314) 2024-10-04 22:22:19 +00:00
Amir Raminfar 049214d529 chore: clean up sql and introduce another component (#3312) 2024-10-04 20:48:22 +00:00
Amir Raminfar d11f997999 chore: updates duckdb 2024-10-04 13:47:59 -07:00
Amir Raminfar 1e5264a00b feat: adds support for levels (#3306) 2024-10-04 13:47:12 -07:00
Amir Raminfar 157b2cb304 fix: fixes broken download logs for agent. fixes #3308 (#3311) 2024-10-04 20:21:11 +00:00
renovate[bot] dc18e0f4a4 fix(deps): update all non-major dependencies (#3310)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-04 14:47:57 +00:00
Amir Raminfar 9bbe35e7fd perf: fixes performance issue with sql table 2024-10-03 18:29:22 -07:00
renovate[bot] add108dc04 chore(deps): update all non-major dependencies (#3307)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-03 23:14:17 +00:00
Amir Raminfar 45ec05419d chore(docs): updates remote info 2024-10-02 08:08:10 -07:00
renovate[bot] 00f0cb606a fix(deps): update all non-major dependencies (#3303)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-02 15:01:37 +00:00
Amir Raminfar 896c402adc chore: adds static check as a different build step (#3305) 2024-10-02 07:57:01 -07:00
Amir Raminfar f9f0f8f1e0 chore(docs): updates faqs 2024-10-02 07:50:08 -07:00
James O'Claire 3b014dc0c7 docs: Add Show Stopped Containers to Dozzle (#3304) 2024-10-02 07:39:33 -07:00
Amir Raminfar 3dcd5b3b78 chore: updates go version 2024-10-02 07:35:57 -07:00
Amir Raminfar ea8a9401ee chore: release v8.5.5 2024-10-01 07:48:04 -07:00
Amir Raminfar 36e5abfa64 fix: supports empty values for logfmt (#3300) 2024-09-30 18:17:17 +00:00
renovate[bot] b0a4a4c6e4 chore(deps): update all non-major dependencies (#3299)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-30 15:11:59 +00:00
renovate[bot] 96366db7da fix(deps): update dependency unplugin-vue-macros to ^2.12.3 (#3296)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-30 00:10:39 +00:00
Amir Raminfar 1570537d6c chore: release v8.5.4 2024-09-29 08:27:05 -07:00
Amir Raminfar 45e09aae6d chore: updates duckdb 2024-09-29 08:24:18 -07:00
renovate[bot] abc4055a09 chore(deps): update all non-major dependencies (#3295)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-28 19:27:16 +00:00
Elias Axelsson f01cec0b39 fix: guess log level will also check severity (#3294) 2024-09-28 12:20:30 -07:00
Amir Raminfar a77c741006 chore: release v8.5.3 2024-09-26 16:54:57 -07:00
Amir Raminfar de79f03aa3 feat: uses bcrypt hash instead (#3293) 2024-09-26 16:40:47 -07:00
Amir Raminfar 3c15ac225c chore: release v8.5.2 2024-09-26 10:32:24 -07:00
renovate[bot] 9aeb5164f2 chore(deps): update all non-major dependencies (#3292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Amir Raminfar <findamir@gmail.com>
2024-09-26 17:31:26 +00:00
Amir Raminfar 3497bf37d1 chore: fixes typescheck 2024-09-26 10:26:13 -07:00
Amir Raminfar f600d264bc chore: updates duckdb 2024-09-26 09:55:50 -07:00
Amir Raminfar e48aec12f5 feat: shows first 1000 records now 2024-09-26 09:54:51 -07:00
Huang Huan 412b599bcd chore: Update zh.yml to newest version (#3291) 2024-09-26 07:57:17 -07:00
renovate[bot] dc45c79402 chore(deps): update all non-major dependencies (#3290)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-25 16:45:08 +00:00
Amir Raminfar 0f753d37fc chore: adds timeout to suspense 2024-09-24 14:11:59 -07:00
renovate[bot] a79b786faf chore(deps): update all non-major dependencies (#3289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-24 21:02:18 +00:00
Amir Raminfar 1fd7f683b2 chore: ignore duckdb 2024-09-22 08:45:09 -07:00
Amir Raminfar b666a842f6 chore: updates duckdb 2024-09-22 08:42:50 -07:00
Amir Raminfar 721d774f8e chore: adds duckdb-wasm to ignore 2024-09-22 08:42:30 -07:00
renovate[bot] 027b25cce5 fix(deps): update all non-major dependencies (#3286)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-22 15:39:55 +00:00
Amir Raminfar d3a5324f0f chore: updates docs 2024-09-21 14:24:07 -07:00
Amir Raminfar 0f8bea46af chore: ignore errors 2024-09-21 14:19:26 -07:00
Amir Raminfar 0d783c26cb chore: cleans up async operations 2024-09-21 08:15:05 -07:00
Amir Raminfar 4b74944e52 chore: updates duckdb 2024-09-20 19:10:39 -07:00
Amir Raminfar 02ca609d16 chore: updates readme 2024-09-20 17:25:16 -07:00
Amir Raminfar 64b542131a chore: updates documentation 2024-09-20 17:14:13 -07:00
Amir Raminfar 6343629457 chore: adds timeout 2024-09-20 16:53:27 -07:00
Amir Raminfar dfaa52e2a6 chore: removes go-json (#3284) 2024-09-20 17:02:00 +00:00
Amir Raminfar 0f3585a3f4 chore: release v8.5.1 2024-09-20 09:45:23 -07:00
renovate[bot] ddddc6e512 chore(deps): update all non-major dependencies (#3283)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-20 16:24:40 +00:00
Amir Raminfar 92614ea63c perf: introduces a timeout when fetching host info (#3276) 2024-09-20 16:19:08 +00:00
Amir Raminfar 03fdd82db6 chore: release v8.5.0 2024-09-19 08:08:29 -07:00
renovate[bot] 9421738fd4 chore(deps): update all non-major dependencies (#3281)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-19 00:38:07 +00:00
Amir Raminfar 90b9d97685 feat: duckdb wasm capibility (#3272) 2024-09-18 17:27:46 -07:00
Amir Raminfar f1553e33e1 chore: release v8.4.4 2024-09-17 13:07:18 -07:00
renovate[bot] b7d376c45b chore(deps): update all non-major dependencies (#3280)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-17 17:47:06 +00:00
Amir Raminfar 66d8064462 fix: fixes a css bug that was causing browser horizontal scrollbar. see #3278 (#3279) 2024-09-17 05:55:03 -07:00
renovate[bot] 2db41e044f fix(deps): update dependency date-fns to v4 (#3277) 2024-09-15 20:16:59 -07:00
renovate[bot] 70b4aca23b chore(deps): update all non-major dependencies (#3274)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-14 16:06:31 +00:00
Amir Raminfar df35f57ccf chore: clean up 2024-09-14 08:59:30 -07:00
Amir Raminfar f8f57d4d9a chore: generate import 2024-09-14 08:58:40 -07:00
renovate[bot] b777136449 fix(deps): update all non-major dependencies (#3271)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-13 15:33:06 +00:00
renovate[bot] a482be0c4c fix(deps): update all non-major dependencies (#3270)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-11 15:23:41 +00:00
renovate[bot] 5cc0ba5388 fix(deps): update dependency @intlify/unplugin-vue-i18n to v5 (#3268) 2024-09-10 14:14:57 +00:00
renovate[bot] a53346a30d fix(deps): update dependency vue-i18n to v10 (#3269) 2024-09-10 14:06:20 +00:00
renovate[bot] 0f15a6af95 chore(deps): update all non-major dependencies (#3267)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-10 13:29:32 +00:00
renovate[bot] 1246553e8d fix(deps): update all non-major dependencies (#3264)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-09 18:05:49 +00:00
Amir Raminfar c528aa1d3d chore: release v8.4.3 2024-09-09 10:56:05 -07:00
Amir Raminfar e841b6cba7 perf: improves animation css to use translate instead 2024-09-09 10:39:54 -07:00
Amir Raminfar 8183b9e70f fix: cleans up login page (#3263) 2024-09-09 15:28:03 +00:00
renovate[bot] 141874b6a5 fix(deps): update dependency unplugin-vue-router to ^0.10.8 (#3262)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-09 00:34:34 +00:00
Amir Raminfar d6477848d5 fix: cleans up on hover background color 2024-09-08 14:10:48 -07:00
Amir Raminfar 293bf9e021 chore: release v8.4.2 2024-09-08 09:05:10 -07:00
Amir Raminfar fa87939676 chore: updates readme 2024-09-08 09:02:31 -07:00
Amir Raminfar d482dcd667 chore: updates modules 2024-09-08 09:00:09 -07:00
Amir Raminfar 65cdb306f8 chore: adds fatal error message when event.message is nil (#3259) 2024-09-08 08:57:24 -07:00
Amir Raminfar 7179b630a5 chore: improves animation 2024-09-08 08:54:16 -07:00
Amir Raminfar 8ee18ce9f4 more fixes 2024-09-07 16:24:42 -07:00
Amir Raminfar a5ffd88884 fix: improves search by backfilling on first search (#3261) 2024-09-07 22:11:09 +00:00
renovate[bot] 328a277cff fix(deps): update all non-major dependencies (#3260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-07 14:12:21 +00:00
renovate[bot] 0d58f2bfca fix(deps): update all non-major dependencies (#3241)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-06 14:51:11 +00:00
Amir Raminfar 73414c6a8d chore: updates go 1.23.1 (#3258) 2024-09-06 07:41:02 -07:00
Amir Raminfar 95fafd12c5 chore: updates go 1.23.1 2024-09-06 06:31:57 -07:00
Amir Raminfar cdf6334cef chore: skip vue errors 2024-09-05 11:42:38 -07:00
Amir Raminfar 6791c53d43 fix: fixes search while looking back history (#3256) 2024-09-05 09:48:13 -07:00
Amir Raminfar 0cbc08fa1a chore: updates vue (#3255) 2024-09-05 08:30:18 -07:00
Amir Raminfar 45c7224d13 fix: improves search on initital load (#3253) 2024-09-05 00:22:30 +00:00
Amir Raminfar 51a3f1f03b chore: updates vue (#3252) 2024-09-04 17:00:27 -07:00
Amir Raminfar 9ddbe6fbb0 feat: shows raw json in complex logs with option to copy (#3251) 2024-09-04 08:53:40 -07:00
Amir Raminfar a62331a488 chore: release v8.4.1 2024-09-03 13:56:59 -07:00
Amir Raminfar 2a3af94467 fix: fixes merged view and incorrect base. fixes #3248 (#3249) 2024-09-03 20:54:10 +00:00
Amir Raminfar 42e423fd2e chore: release v8.4.0 2024-09-03 09:02:48 -07:00
Amir Raminfar 9a146ae62a chore: fixes docs 2024-09-03 09:01:21 -07:00
Amir Raminfar 1518d80aad chore: updates to vue 3.5 2024-09-03 08:55:19 -07:00
Amir Raminfar a1efb8e340 fix: supports searching for int, float and boolean (#3247) 2024-09-03 15:53:00 +00:00
Amir Raminfar 7d42bd2b02 chore: adds comparison chart for agents and removes warning (#3246) 2024-09-03 15:13:42 +00:00
Amir Raminfar 9f2f8b8245 feat!: moves search to backend for better experience. It does remove show search results in context. (#3245) 2024-09-03 08:02:09 -07:00
Amir Raminfar 057d7ad712 chore: updates modules 2024-09-01 15:41:16 -07:00
Amir Raminfar 712e1d389a chore: updates vue component compiler 2024-08-30 09:44:22 -07:00
Amir Raminfar c1fcafba43 chore: updates vue 2024-08-30 09:43:58 -07:00
Amir Raminfar 74063a0c72 feat: makes improvements to mobile view (#3244) 2024-08-30 09:36:27 -07:00
Amir Raminfar 12e7ee33b7 chore: release v8.3.0 2024-08-29 13:23:06 -07:00
Amir Raminfar db33e28117 chore: fixes wrap 2024-08-29 13:11:22 -07:00
Amir Raminfar df1a650980 chore: adds pinned headers 2024-08-29 13:07:00 -07:00
Amir Raminfar a937615747 chore: adds all toggle 2024-08-29 13:04:31 -07:00
Amir Raminfar d13163b0d9 chore: removes tag if not there for level (#3242) 2024-08-29 15:59:50 +00:00
Amir Raminfar 9ea7024c2a feat: show version number on top right. see #3228 (#3239) 2024-08-29 00:39:24 +00:00
Amir Raminfar 85271a595a feat: adds a side panel for complex logs that enables reordering and enabling fields (#3237) 2024-08-29 00:37:04 +00:00
renovate[bot] 62a2ef9eb4 fix(deps): update all non-major dependencies (#3238)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-28 15:52:03 +00:00
Amir Raminfar 64790daea7 chore: const 2024-08-26 14:23:04 -07:00
Amir Raminfar 0798de3b3a fix: fixes broken skip logs 2024-08-26 13:42:19 -07:00
Amir Raminfar 0a03923587 feat: changes scroll progress to reflect total available logs instead of just the logs in the browser (#3236) 2024-08-26 20:12:24 +00:00
Amir Raminfar 3a0eaf1609 fix: fixes infinite loop when there are no logs (#3235) 2024-08-26 18:22:43 +00:00
Amir Raminfar a6620ff315 chore: fixes broken settings page 2024-08-26 08:13:13 -07:00
Amir Raminfar aec8139a19 feat: adds animations to bottom when in live mode (#3231) 2024-08-26 14:58:39 +00:00
Amir Raminfar afcb80938e feat: retries to fetch more logs if no logs are fetched when scrolling up (#3230) 2024-08-26 06:23:11 -07:00
Amir Raminfar 36f3308e1d perf: tries to improve burst of logs (#3227) 2024-08-26 06:22:50 -07:00
renovate[bot] b6091ff944 chore(deps): update dependency jsdom to v25 (#3233) 2024-08-26 00:51:35 +00:00
renovate[bot] 4bb007fdfd fix(deps): update all non-major dependencies (#3232)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-26 00:22:44 +00:00
Amir Raminfar a56fea1728 chore: updates makefile 2024-08-25 08:39:10 -07:00
renovate[bot] 6a1a412f47 chore(deps): update all non-major dependencies (#3224)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-23 16:25:45 +00:00
Qhilm eaf20059ba fix: host port nb typo in readme (#3220) 2024-08-21 09:11:56 -07:00
renovate[bot] a0db92401f chore(deps): update mcr.microsoft.com/playwright docker tag to v1.46.1 (#3218)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-19 15:19:17 +00:00
Amir Raminfar 3f8728c821 chore: fixes typecheck by reverting hoisting in npmrc (#3219) 2024-08-19 15:08:09 +00:00
Amir Raminfar 4319415e1d fix: fixes settings page when changing language (#3216) 2024-08-16 19:34:14 +00:00
renovate[bot] b54611fb65 fix(deps): update dependency unplugin-vue-macros to ^2.11.6 (#3215)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-16 15:45:03 +00:00
Cololi b0012633aa chore: Complete Simplified and Traditional Chinese translations (#3214) 2024-08-16 08:39:05 -07:00
Amir Raminfar 8cbd04812a bug: fixes broken alt enter on fuzzy search 2024-08-15 15:46:34 -07:00
renovate[bot] 1dea4f1059 fix(deps): update dependency @vueuse/router to v11 (#3213)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-15 15:40:27 -07:00
renovate[bot] 3d9e025b39 fix(deps): update dependency @vueuse/components to v11 (#3210)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-15 22:20:40 +00:00
renovate[bot] acd9946bc8 fix(deps): update dependency @vueuse/core to v11 (#3211)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-15 22:02:00 +00:00
renovate[bot] 9f743132dd fix(deps): update all non-major dependencies (#3209)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-15 21:58:25 +00:00
Amir Raminfar 621a3d98bd chore: release v8.2.1 2024-08-15 11:01:40 -07:00
renovate[bot] bf574c6b83 chore(deps): update all non-major dependencies (#3208)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-15 17:31:36 +00:00
Florian 9ace6eccdf feat: Translate the example log entries (#3206) 2024-08-15 08:20:45 -07:00
Florian 3d152b13dc bug: Translated incorrect into German correctly and added missing (#3205) 2024-08-14 19:21:40 +00:00
renovate[bot] 10f868d2c4 chore(deps): update all non-major dependencies (#3201)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-14 17:12:56 +00:00
Amir Raminfar 077eeb7917 chore: uses zerolog instead of logrus (#3203) 2024-08-14 17:04:18 +00:00
Amir Raminfar 37f0249065 chore: removes healthcheck analytics 2024-08-13 13:06:52 -07:00
Amir Raminfar aacf0474e4 chore: fixes broken anallytics for generate 2024-08-13 13:00:29 -07:00
Amir Raminfar c1bf6b15c9 chore: release v8.2.0 2024-08-13 12:43:08 -07:00
Amir Raminfar fa7c7ca62d docs: updates agents custom certs 2024-08-13 12:42:32 -07:00
Amir Raminfar 0d3ed03e04 chore: updates makefile 2024-08-13 11:49:24 -07:00
Amir Raminfar 7f735d26db feat: reads certs locally if available (#3196) 2024-08-13 11:48:56 -07:00
Amir Raminfar 9467e3d7d5 chore: update readme (#3199) 2024-08-13 15:47:59 +00:00
Amir Raminfar 3072709f7e chore: updates readme 2024-08-13 08:30:46 -07:00
renovate[bot] d1a308b3b6 chore(deps): update all non-major dependencies (#3198)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-13 15:22:58 +00:00
Amir Raminfar 9dfdfbf8b4 chore: removes version from yml files and docs 2024-08-13 08:15:03 -07:00
Jérémy Touati acbb2a8d24 feat: implements support for custom container names (#3197) 2024-08-13 08:04:51 -07:00
Amir Raminfar 6c5dcb7a62 chore: release v8.1.5 2024-08-12 09:43:02 -07:00
Amir Raminfar aa824453a5 feat: supports any service name in swarm mode by looking internally the hostname (#3193) 2024-08-12 09:34:53 -07:00
renovate[bot] bdf19587f2 chore(deps): update all non-major dependencies (#3195)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-12 15:35:40 +00:00
Amir Raminfar 1637b37cc1 chore: hides drop down menu on mobile 2024-08-11 08:54:08 -07:00
renovate[bot] 4eb73e96c5 chore(deps): update all non-major dependencies (#3191)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-10 20:41:25 +00:00
Amir Raminfar 69fb227519 feat: adds action toolbar for groups, services and stacks (#3190) 2024-08-10 13:35:56 -07:00
renovate[bot] ffc4559047 fix(deps): update all non-major dependencies (#3188)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-08 17:44:48 +00:00
renovate[bot] 05fc3f04ec chore(deps): update all non-major dependencies (#3186)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-07 17:06:13 +00:00
renovate[bot] 84be3b1648 chore(deps): update all non-major dependencies (#3185)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-07 07:20:47 +00:00
renovate[bot] cabe2c803a chore(deps): update all non-major dependencies (#3184)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-06 14:14:18 +00:00
Amir Raminfar f5a9669249 chore: release v8.1.4 2024-08-05 14:47:50 -07:00
renovate[bot] c382b25b97 fix(deps): update dependency vitepress to v1.3.2 (#3171)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-05 21:08:00 +00:00
Amir Raminfar 9db280e7b4 chore: updates vue-router by adding hoisting (#3181) 2024-08-05 21:00:52 +00:00
Amir Raminfar 22c1473c4b fix: fixes search skipping every other log (#3180) 2024-08-05 12:50:40 -07:00
Amir Raminfar f53c4d7d3d chore: updates modules (#3179) 2024-08-04 16:31:24 -07:00
Amir Raminfar 2514c666c4 chore: updates node modules (#3177) 2024-08-03 15:48:09 +00:00
Amir Raminfar b38a02f5ba fix: fixes broken image tag in title bar (#3176) 2024-08-03 00:24:18 +00:00
Amir Raminfar db1cccc61f chore: updates vue 2024-08-01 09:05:12 -07:00
Amir Raminfar db75e0f3e9 chore: updates vue/vite (#3174) 2024-08-01 09:02:01 -07:00
Amir Raminfar fe149dc8b2 chore: updates vitest (#3175) 2024-08-01 15:59:57 +00:00
Amir Raminfar 3a77e2b187 chore: updates node (#3173) 2024-08-01 08:43:37 -07:00
Amir Raminfar 6d4be52006 fix: fixes missing info (#3172) 2024-08-01 15:36:33 +00:00
Amir Raminfar ddcd625a70 chore: release v8.1.3 2024-07-31 10:48:36 -07:00
Amir Raminfar 1b9f25e64f chore: fixes analytics to have better data (#3170) 2024-07-31 17:39:24 +00:00
Steven Horner 73fa622721 docs: Fix syntax to enable actions (#3169) 2024-07-31 05:58:57 -07:00
renovate[bot] 0e652cb936 chore(deps): update all non-major dependencies (#3168)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-31 02:37:07 +00:00
Amir Raminfar 657643e6ce fix: fixes panic in agent with stopped containers (#3167) 2024-07-30 19:31:14 -07:00
Amir Raminfar b4277603cf chore: fixes title truncate (#3165) 2024-07-30 12:10:17 -07:00
Amir Raminfar d72edefac2 chore: updates swarm file 2024-07-30 07:48:41 -07:00
Amir Raminfar df4b449bc4 chore: release v8.1.2 2024-07-29 13:10:02 -07:00
Amir Raminfar 74fda04ebb chore: adds swarm mode as analytics 2024-07-29 12:18:58 -07:00
Amir Raminfar 638889ed58 feat: supports container actions for agents (#3164) 2024-07-29 19:16:38 +00:00
renovate[bot] f09e294f38 fix(deps): update all non-major dependencies (#3162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-29 01:54:46 +00:00
Amir Raminfar de2e757665 fix: refreshes versions for agent properly after restart (#3160) 2024-07-28 14:55:15 -07:00
Amir Raminfar ed84b9a181 perf: uses a semaphore to fetch containers faster in parallel (#3161) 2024-07-28 14:54:45 -07:00
Amir Raminfar c906b80f52 fix: fixes improper version handeling (#3157) 2024-07-28 00:32:38 +00:00
Amir Raminfar e2a951ca3d chore: release v8.1.1 2024-07-27 16:54:01 -07:00
Amir Raminfar c91adb221a fix: fixes default fallback not working. fixes #3154 (#3156) 2024-07-27 16:46:11 -07:00
Amir Raminfar 989f0f03ee feat: uses desktop menu instead for mobile menu (#3153) 2024-07-26 11:18:05 -07:00
Amir Raminfar ba10c03a90 chore: release v8.1.0 2024-07-26 09:32:46 -07:00
Amir Raminfar 41859e64a5 chore: wraps icons 2024-07-26 08:10:58 -07:00
Amir Raminfar 5ef18bf508 chore: fixes sizing on icon 2024-07-26 08:04:31 -07:00
Amir Raminfar 19d0355d7c perf: only loads selected locales (#3152) 2024-07-25 17:29:16 -07:00
Amir Raminfar cbed761a18 perf: limits maximum items to be fetched when scrolling up. see #3122 (#3151) 2024-07-25 15:26:28 +00:00
renovate[bot] abd9cebdbe chore(deps): update all non-major dependencies (#3150)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-25 15:08:22 +00:00
Amir Raminfar 3a988b8fdc feat: updates agents to be more resilient by reconnecting. also adds big performance issues in swarm mode with little updates to the UI. (#3145) 2024-07-25 08:01:33 -07:00
Amir Raminfar c87a7b1272 docs: fixes agent page 2024-07-24 19:42:02 -07:00
Amir Raminfar ed351f9d07 chore: release v8.0.7 2024-07-24 08:25:15 -07:00
renovate[bot] c4ff571eb5 chore(deps): update all non-major dependencies (#3148)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-24 15:15:56 +00:00
Amir Raminfar 2bd0031feb chore: fixes async bug 2024-07-24 08:03:25 -07:00
Amir Raminfar 082bed2382 perf: fixes an issue with too many containers (#3144) 2024-07-24 06:58:21 -07:00
Amir Raminfar 4aa315a471 fix: fixes first few lines not showing up. fixes #3139 (#3142) 2024-07-23 21:52:46 -07:00
Amir Raminfar 79118368b1 chore: adds more logs for authentication (#3143) 2024-07-23 13:05:17 -07:00
renovate[bot] daf8cf815b chore(deps): update all non-major dependencies (#3137)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-22 21:02:26 +00:00
Amir Raminfar d2015fdebd chore: cleans up logging context (#3136) 2024-07-22 20:57:27 +00:00
renovate[bot] 989a650f12 chore(deps): update pnpm to v9.6.0 (#3132)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-22 00:26:28 +00:00
Amir Raminfar a95f8a7ae2 chore: release v8.0.6 2024-07-21 07:45:00 -07:00
renovate[bot] ab2fb9511e chore(deps): update all non-major dependencies (#3131)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-21 14:32:13 +00:00
Amir Raminfar 5d9017be01 fix: fetching more should only allow for one and abort if one is already in progress. fixes #3113 (#3118) 2024-07-20 15:51:19 -07:00
Amir Raminfar aa6556be22 fix: fixes broken auto redirect. see #3123 (#3129) 2024-07-20 15:01:01 -07:00
renovate[bot] 8359985d23 chore(deps): update all non-major dependencies (#3124)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-19 16:26:18 +00:00
Amir Raminfar 808f50dbcd chore: adds compose file to bug issue 2024-07-18 09:11:02 -07:00
renovate[bot] 565bfa302a fix(deps): update dependency @iconify-json/material-symbols to ^1.1.85 (#3121)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-18 15:33:15 +00:00
renovate[bot] e0f567aace chore(deps): update all non-major dependencies (#3120)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-18 00:26:15 +00:00
Amir Raminfar 01afadd410 fix: uses correct int size for memory (#3115) 2024-07-16 06:03:31 -07:00
renovate[bot] 987c0e6cc4 fix(deps): update dependency tailwindcss to ^3.4.5 (#3117)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-15 16:55:47 +00:00
Amir Raminfar 168344d812 chore: fixes broken error with macros (#3116) 2024-07-15 09:48:04 -07:00
renovate[bot] 9f02b3ac1c chore(deps): update all non-major dependencies (#3114)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-15 14:57:17 +00:00
renovate[bot] 59e0b51c1a fix(deps): update all non-major dependencies (#3112)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-15 00:59:39 +00:00
Amir Raminfar 6c153051c9 fix: fixes broken host info for remote hosts. fixes #3103 2024-07-14 15:46:02 -07:00
renovate[bot] c5e3fbcd3f fix(deps): update all non-major dependencies (#3111)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-14 02:05:46 +00:00
Amir Raminfar d138f6689f docs: updates warnings 2024-07-13 18:23:29 -07:00
Amir Raminfar 64e8ec48e4 docs: adds warning for duplicate hosts 2024-07-13 14:45:42 -07:00
Nikolaos Pothitos e9ceff0296 docs: fix typo (#3110) 2024-07-13 06:34:36 -07:00
Amir Raminfar 749886bbbb docs: updates env vars 2024-07-12 12:20:03 -07:00
Amir Raminfar 71ff516779 docs: updates env vars 2024-07-12 12:10:32 -07:00
Amir Raminfar 983c956ade chore: change files 2024-07-12 11:53:44 -07:00
Amir Raminfar 46c62c878f chore: adds faq 2024-07-12 11:49:04 -07:00
Amir Raminfar 09ff535fea chore: more templates 2024-07-12 11:47:23 -07:00
Amir Raminfar 6a07f13896 chore: more templates 2024-07-12 11:45:13 -07:00
Amir Raminfar 369b1a68ee Update issue templates 2024-07-12 11:43:49 -07:00
Amir Raminfar bcdc79eb43 Update issue templates (#3107) 2024-07-12 11:43:11 -07:00
Amir Raminfar 0947073e7d chore: more templates 2024-07-12 11:38:55 -07:00
Amir Raminfar 63dc37897a chore: more templates 2024-07-12 11:37:17 -07:00
Amir Raminfar 24280648ae chore: change files 2024-07-12 10:32:29 -07:00
Amir Raminfar 7fcf71203f chore: more templates 2024-07-12 10:29:50 -07:00
renovate[bot] 8fec434613 fix(deps): update all non-major dependencies (#3106)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-12 17:20:48 +00:00
Amir Raminfar 3104195bf2 chore: adds config.yml 2024-07-12 10:17:36 -07:00
Amir Raminfar a469f5ef36 chore: adds new bug template (#3105) 2024-07-12 10:09:08 -07:00
Amir Raminfar cf2845b29a docs: update host id in FAQ (#3102) 2024-07-12 02:16:43 +00:00
Amir Raminfar 3541c326bf feat: auto links url for simple logs. fixes #3063 (#3100) 2024-07-11 15:42:50 -07:00
Amir Raminfar 15685ce954 chore: release v8.0.5 2024-07-11 12:47:14 -07:00
Amir Raminfar ec31d60fb0 fix: missed a few anlaytics that were not respecting no analytics flag (#3099) 2024-07-11 19:29:53 +00:00
Amir Raminfar 5af611ab44 feat: removes orphaned containers. fixes #3077 (#3098) 2024-07-11 19:16:47 +00:00
Amir Raminfar c5771fa9a4 feat: tries to use swarm node id if exists then machine-id (#3097) 2024-07-11 19:09:49 +00:00
Amir Raminfar 8277902020 chore: improves errors 2024-07-11 11:05:26 -07:00
Amir Raminfar 208671ef23 feat: sorts services in swarm view (#3096) 2024-07-11 10:56:08 -07:00
Amir Raminfar 4077d5ab47 chore: updates readme 2024-07-11 08:04:47 -07:00
Amir Raminfar e946e371c5 chore: updates makefile for dev 2024-07-11 08:03:36 -07:00
Amir Raminfar 44fb55ddfc docs: quick fix 2024-07-10 11:11:48 -07:00
Amir Raminfar eb07da62ed chore: uses make generate for int tests instead (#3092) 2024-07-10 09:16:40 -07:00
Amir Raminfar 7c3a7826bc chore: release v8.0.4 2024-07-10 08:53:25 -07:00
Amir Raminfar d82e00e877 fix: fixes broken versions. fixes #3087. (#3084) 2024-07-10 15:52:54 +00:00
bluepuma77 c4b5c1c193 docs: Update README.md (#3090) 2024-07-10 08:18:17 -07:00
Amir Raminfar da709df7f4 chore: release v8.0.3 2024-07-09 17:11:35 -07:00
renovate[bot] 9099ffce38 chore(deps): update node.js to v22.4.1 (#3081)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-09 22:48:34 +00:00
Amir Raminfar cdb7855578 docs: updates docs (#3083) 2024-07-09 22:43:40 +00:00
renovate[bot] 536a3a630f chore(deps): update dependency vitest to v2 (#3082)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-09 20:59:51 +00:00
Amir Raminfar b8662ff8d5 fix: properly closes unused connections (#3080) 2024-07-09 13:02:15 -07:00
Amir Raminfar 09d5470053 chore: release v8.0.2 2024-07-09 09:59:12 -07:00
Amir Raminfar f866724f62 chore: adds more examples 2024-07-09 09:42:21 -07:00
Amir Raminfar ce2b008098 fix: improves memory leak in new container stream (#3076) 2024-07-09 15:14:28 +00:00
renovate[bot] 00f2c4f2ef fix(deps): update all non-major dependencies (#3073)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-08 02:37:21 +00:00
Amir Raminfar a4ee0c82fd docs: adds custom groups 2024-07-07 11:00:34 -07:00
Amir Raminfar c2d3e83c55 docs: adds more examples 2024-07-07 10:49:13 -07:00
Amir Raminfar 30347b0491 chore: typo 2024-07-07 08:23:08 -07:00
Amir Raminfar af92c5e0ea docs: adds tip for swarm mode 2024-07-07 08:18:25 -07:00
Amir Raminfar 3373703114 docs: updates text 2024-07-06 18:36:25 -07:00
Amir Raminfar 6246b866d5 fix: removes empty stacks (#3071) 2024-07-06 14:22:32 -07:00
Amir Raminfar ad5d3e8e75 docs: deprecation 2024-07-06 14:05:46 -07:00
Amir Raminfar 044c1db55d chore: refactors main.go and moves cli args to a new file (#3070) 2024-07-06 17:51:36 +00:00
renovate[bot] 9535b80c10 chore(deps): update all non-major dependencies (#3068)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-06 16:51:08 +00:00
Amir Raminfar 8b367010ce chore: updates gitignore to checkin generated files 2024-07-06 09:45:32 -07:00
Amir Raminfar 4ba989e52a Update agent.md 2024-07-05 14:45:10 -07:00
Amir Raminfar abac919aaa chore: release v8.0.1 2024-07-05 13:51:55 -07:00
Amir Raminfar 178aeeb77f chore: fixes deploy 2024-07-05 13:47:52 -07:00
Amir Raminfar c83fe15bf5 docs:updates links to deprecation 2024-07-05 13:46:06 -07:00
Amir Raminfar 7a1424bdd4 chore: release v8.0.0 2024-07-05 13:40:23 -07:00
Amir Raminfar 4de9c775ba feat!: implements swarm mode with agents (#3058) 2024-07-05 13:38:10 -07:00
renovate[bot] 2e5fb71938 fix(deps): update all non-major dependencies (#3065)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-04 17:23:20 +00:00
renovate[bot] 26e4d8810b fix(deps): update all non-major dependencies (#3064)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-01 01:22:52 +00:00
Amir Raminfar 48fc3d3c74 chore: updates docker to latest client and fixes deprecations (#3062) 2024-06-28 21:17:45 +00:00
renovate[bot] 89f8530620 fix(deps): update dependency entities to v5 (#3060)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-28 19:45:59 +00:00
renovate[bot] 2fe8e1ab6c chore(deps): update all non-major dependencies (#3059)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-28 19:37:06 +00:00
renovate[bot] 9b87cd929c fix(deps): update dependency daisyui to ^4.12.7 (#3056)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-24 01:40:28 +00:00
renovate[bot] 806af34a2d fix(deps): update all non-major dependencies (#3055)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-23 15:13:38 +00:00
renovate[bot] 3b1801a10a chore(deps): update all non-major dependencies (#3054)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-22 15:54:02 +00:00
Amir Raminfar ed49153af7 chore: release v7.0.7 2024-06-21 12:35:06 -07:00
Amir Raminfar 0c596262f0 chore: remove unused 2024-06-21 12:15:34 -07:00
Amir Raminfar 3d19870b36 chore: fixes bugs with auto switching of menu 2024-06-21 12:04:22 -07:00
Amir Raminfar ae960bac05 chore: fixes type on routes 2024-06-21 11:20:07 -07:00
renovate[bot] 217db5ff54 fix(deps): update all non-major dependencies (#3053)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-21 11:14:29 -07:00
codeman1o1 f0b44a2608 feat: adds critical (#3051) 2024-06-20 06:32:36 -07:00
Amir Raminfar 9465253baf docs: adds search and icons 2024-06-19 15:16:16 -07:00
renovate[bot] e41a981710 chore(deps): update all non-major dependencies (#3049)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-19 15:03:54 +00:00
renovate[bot] 98bbc245dc chore(deps): update docker/bake-action action to v5 (#3046)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-18 08:10:48 -07:00
renovate[bot] 1fe510dc35 chore(deps): update docker/build-push-action action to v6 (#3047)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-18 08:10:40 -07:00
renovate[bot] 197bf2fa53 chore(deps): update all non-major dependencies (#3045)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-18 14:17:09 +00:00
Amir Raminfar 5d68080db2 chore: reverts docs 2024-06-17 19:11:22 -07:00
Amir Raminfar 0d613a7c97 Revert "chore: fix bug"
This reverts commit 37c221f89c.
2024-06-17 19:10:29 -07:00
Amir Raminfar baf63ca9f1 Revert "docs: updates fonts (#3042)"
This reverts commit 8e894f0e65.
2024-06-17 19:08:28 -07:00
Amir Raminfar 37c221f89c chore: fix bug 2024-06-17 17:09:51 -07:00
Amir Raminfar 8e894f0e65 docs: updates fonts (#3042) 2024-06-18 00:08:04 +00:00
Amir Raminfar f24d4951ad chore: upgrade to unplug-vue-router (#3041) 2024-06-17 12:07:24 -07:00
Amir Raminfar d344c5f20e docs: removes shadow (#3040) 2024-06-17 17:15:00 +00:00
renovate[bot] 3af15ac43e chore(deps): update all non-major dependencies (#3039)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-16 19:12:57 +00:00
Amir Raminfar 513e51134b docs: star count docs (#3037) 2024-06-15 20:39:00 +00:00
renovate[bot] 0bb5395d28 chore(deps): update all non-major dependencies (#3034)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-14 13:10:08 +00:00
renovate[bot] 58903612fc chore(deps): update all non-major dependencies (#3033)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-13 17:43:03 +00:00
Amir Raminfar 6452015d8a chore: release v7.0.6 2024-06-12 13:59:35 -07:00
Amir Raminfar 4b48b73fb9 fix: adds missing . in swarm id for containter title 2024-06-12 12:50:33 -07:00
renovate[bot] 1898b87b15 fix(deps): update all non-major dependencies (#3032)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-12 19:50:29 +00:00
Amir Raminfar f9c29e2cbc fix: improves keyboard navigation and styling of fuzzy search (#3031) 2024-06-12 12:43:47 -07:00
Amir Raminfar 214b1825a8 feat: improves search by showing all matches within threshold and enable scrolling instead (#3029) 2024-06-12 09:23:10 -07:00
renovate[bot] 19ee1fedc4 chore(deps): update all non-major dependencies (#3030)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-12 01:39:02 +00:00
Amir Raminfar 2355fdb152 chore: code clean up 2024-06-11 07:55:44 -07:00
renovate[bot] 51a2bb5207 chore(deps): update dependency c8 to v10 (#3025) 2024-06-10 17:05:30 -07:00
renovate[bot] 34ee472fca chore(deps): update all non-major dependencies (#3024)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-10 23:58:54 +00:00
Amir Raminfar 9447b1e3c9 feat: display top 10 in fuzzysearch (#3023) 2024-06-10 15:16:34 -07:00
renovate[bot] 6aabe0673c chore(deps): update all non-major dependencies (#3022)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-09 22:11:19 +00:00
Amir Raminfar a7309d10bb docs: change to tip 2024-06-09 06:20:21 -07:00
Amir Raminfar 8527c4688d docs: typo 2024-06-09 06:19:21 -07:00
ManiMatter 50a60f04f1 docs: Adjusting compression settings in Traefik (#3020) 2024-06-09 05:57:43 -07:00
renovate[bot] de0913160d fix(deps): update dependency vite to v5.2.13 (#3018)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-07 16:44:18 +00:00
renovate[bot] 6de5cb0fbc fix(deps): update all non-major dependencies (#3016)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-06 17:40:46 +00:00
Amir Raminfar bafce9e6c5 chore: release v7.0.5 2024-06-06 08:35:04 -07:00
Amir Raminfar f1e3a43747 chore: updates build files 2024-06-06 08:26:50 -07:00
Amir Raminfar 220218d44d feat: show start event for containers (#3014) 2024-06-06 08:26:03 -07:00
renovate[bot] 5169c211f3 chore(deps): update all non-major dependencies (#3013)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-05 15:44:31 +00:00
Amir Raminfar 0bab833fa9 fix: fixes nil pointer when message is null (#3012) 2024-06-04 13:51:49 -07:00
Amir Raminfar 895345693c chore: release v7.0.4 2024-06-03 11:52:06 -07:00
Amir Raminfar a0d60357b8 fix: implements logfmt with more strict rules. fixes #3006 (#3007)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-03 15:52:40 +00:00
renovate[bot] f2e29dadf1 chore(deps): update dependency @types/node to ^20.14.0 (#3009)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-03 00:05:49 +00:00
Amir Raminfar 05d6e47d28 chore: fixes missing files 2024-06-02 16:55:42 -07:00
Amir Raminfar 882c5a1186 docs: updates video for dozzle.dev (#3008) 2024-06-02 16:52:39 -07:00
Amir Raminfar 496a894d86 feat: adds severe (#3005) 2024-06-01 21:28:28 +00:00
renovate[bot] e7911dbce6 chore(deps): update dependency prettier to ^3.3.0 (#3004)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-01 20:45:44 +00:00
renovate[bot] 1484318279 chore(deps): update all non-major dependencies (#3002)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-01 15:49:46 +00:00
Amir Raminfar 8dce75509c feat: changes the host when a new container is chosen (#3001) 2024-05-31 16:19:40 -07:00
Amir Raminfar 689c399ace chore: release v7.0.3 2024-05-30 16:51:52 -07:00
Amir Raminfar da62adadc5 fix: fixes a bug when searching for multiple words in fuzzy popup (#3000) 2024-05-30 16:51:20 -07:00
Amir Raminfar 3a30c27c9a feat!: adds services and stacks to fuzzy search. Note labels will no longer be searchable due to a bug on sorting. (#2999) 2024-05-30 09:47:20 -07:00
renovate[bot] 4edac1174c chore(deps): update all non-major dependencies (#2998)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-30 15:03:43 +00:00
Amir Raminfar aeffdcb27f chore: updates node (#2997) 2024-05-30 14:56:12 +00:00
Amir Raminfar 3cb3b395a9 chore: fixes a race condition when fetch is done but not yet processed (#2996) 2024-05-30 14:50:53 +00:00
Amir Raminfar 4829a5e8c4 fix: aborts fetchings more request when container is changed. fixes #2993 (#2995) 2024-05-29 13:29:31 -07:00
Amir Raminfar c2c7041cf7 feat: disables user select for dates on simple and complex logs. fixes #2967 (#2994) 2024-05-29 08:45:02 -07:00
Amir Raminfar 487a4ad0d4 chore: vscode settings 2024-05-29 08:14:25 -07:00
Amir Raminfar e00af4321f chore: extracts more locales text (#2992) 2024-05-28 19:37:41 -07:00
Amir Raminfar ce10266bf2 chore: release v7.0.2 2024-05-28 15:08:15 -07:00
Amir Raminfar 22975208af fix: fixes typo. fixes #2991 2024-05-28 15:06:44 -07:00
Amir Raminfar aed903d57d chore: release v7.0.1 2024-05-28 09:02:29 -07:00
renovate[bot] 98a8f4ac81 chore(deps): update all non-major dependencies (#2990)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-28 15:13:31 +00:00
Amir Raminfar a65f8a2c3a chore: css clean up (#2988) 2024-05-27 20:43:22 +00:00
renovate[bot] 4dd99c209c fix(deps): update all non-major dependencies (#2987)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-27 14:48:20 +00:00
Amir Raminfar 518e3c8ac9 fix: fixes bug when switching between merged logs was not working 2024-05-26 18:40:53 -07:00
renovate[bot] 19c800fc27 chore(deps): update all non-major dependencies (#2986)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-27 00:05:31 +00:00
Amir Raminfar 34dcf9d068 chore: fixes reverse bug on stats 2024-05-26 12:34:55 -07:00
Amir Raminfar beb95c2f87 fix: improves the loading of stats between page changes (#2985) 2024-05-26 17:29:26 +00:00
Amir Raminfar 3b34578e92 feat: adds a simple loader when waiting for logs (#2984) 2024-05-26 10:25:20 -07:00
Amir Raminfar 89d9e67862 fix: fixes when only one container is in a service (#2983) 2024-05-25 15:56:20 -07:00
Amir Raminfar c268fa4e8b fix: bug when a new container in service mode is created (#2982) 2024-05-24 13:04:50 -07:00
Amir Raminfar a089b394e0 chore: updates buy me button 2024-05-24 10:37:10 -07:00
Amir Raminfar 555c0b7222 docs: generates better sitemap 2024-05-24 10:08:30 -07:00
Amir Raminfar 2a36a45fe9 docs: sponsors (#2981) 2024-05-24 09:31:24 -07:00
Amir Raminfar 57bcf874fd chore(docs): updates info on swarm mode 2024-05-24 08:01:48 -07:00
Amir Raminfar 56339b3959 chore: release v7.0.0 2024-05-24 08:01:10 -07:00
Amir Raminfar 2e774481c1 chore: minor fix on padding and regression 2024-05-23 18:27:42 -07:00
Amir Raminfar aed171cb6b fix: fixes padding on mobile (#2980) 2024-05-23 23:25:04 +00:00
Amir Raminfar 4c7f281257 fix: uses better colors for different container names (#2979) 2024-05-23 21:46:57 +00:00
Amir Raminfar 6acb7126f5 fix: fixes menu bug when merging logs (#2978) 2024-05-23 20:26:28 +00:00
Amir Raminfar 9408a9ac45 docs: updates docs for INFO (#2977) 2024-05-23 19:27:59 +00:00
Amir Raminfar f644d57964 feat: shows offline also on dashboard host list 2024-05-23 12:24:58 -07:00
Amir Raminfar 9dfbf7463c docs: adds documentation for swarm mode (#2976) 2024-05-23 11:42:14 -07:00
renovate[bot] 1f19785c99 fix(deps): update all non-major dependencies (#2975)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-23 17:22:23 +00:00
Amir Raminfar ba0206a903 feat: supports swarm mode with stacks and services on remote hosts 🙌🏼 (#2961) 2024-05-23 10:17:16 -07:00
Amir Raminfar 68908e57b7 chore: release v6.6.3 2024-05-23 08:30:20 -07:00
Amir Raminfar b97e0d28cd chore: changes implementation of ordered map (#2974) 2024-05-23 08:26:34 -07:00
Amir Raminfar 1d1f3ed36a feat: keeps the order of json keys. see fixes #2970 (#2971) 2024-05-22 15:59:54 -07:00
renovate[bot] 38352379ab chore(deps): update dependency sitemap to v8 (#2973)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-22 12:30:45 -07:00
renovate[bot] cb602957bc chore(deps): update all non-major dependencies (#2972)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-22 19:25:08 +00:00
Richard Hull 05edd49487 fix: Coerce log level to lowercase when guessing in structured json (#2968) 2024-05-22 06:07:45 -07:00
renovate[bot] b326dca06d chore(deps): update all non-major dependencies (#2966)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-21 18:02:31 +00:00
Amir Raminfar 219e20fdba chore: fixes race condition in container store (#2965) 2024-05-21 10:57:16 -07:00
renovate[bot] bc627a725f chore(deps): update all non-major dependencies (#2952)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-17 23:40:09 +00:00
Amir Raminfar 0c9c09dc4c chore: ignores vue typescript error temporary 2024-05-17 16:31:54 -07:00
Amir Raminfar 5810383a29 fix: only shows event stream error when trying to reconnect (#2958) 2024-05-14 06:13:46 -07:00
Amir Raminfar cbda1e753c chore: fixes race condition (#2959) 2024-05-13 15:32:51 -07:00
LovelessCodes b1f980f49b feat: Danish translation file (#2957) 2024-05-13 13:09:51 -07:00
Amir Raminfar a3310a71f6 chore: release v6.6.2 2024-05-13 08:59:08 -07:00
Amir Raminfar dafc9f674e fix: fixes broken homepage with stats (#2956) 2024-05-13 15:58:52 +00:00
Amir Raminfar 16b1aad93f chore: release v6.6.1 2024-05-13 08:31:41 -07:00
Amir Raminfar b82a0e2fef chore: updates go modules (#2955) 2024-05-13 15:28:29 +00:00
Amir Raminfar 9510a1f79e chore: updates some modules 2024-05-13 08:21:44 -07:00
Marcin Wołosz 8a0166d37d feat: polish language added (#2954) 2024-05-13 07:40:59 -07:00
Amir Raminfar 0a2248dfe3 feat: makes host cards more responsive on mobile (#2949) 2024-05-12 19:45:54 -07:00
Amir Raminfar a712c8fa34 chore: spacing 2024-05-12 13:29:30 -07:00
Amir Raminfar 4b3d2b3224 feat: uses expoentital average for host (#2951) 2024-05-12 20:24:04 +00:00
Fabeuss 2864bafa14 fix: updates fr.yml (#2950) 2024-05-12 13:21:02 -07:00
Freddy Grande 8433f89bd2 feat: Adding debug log when host-specific certificate path not found (#2948) 2024-05-12 05:56:52 -07:00
Freddy Grande bc1a5fbc8b docs: Updated documentation for Remote Host Setup (#2947) 2024-05-12 05:55:16 -07:00
Amir Raminfar 4b046596b7 fix: brings back labels CPU and bases CPU on one core. See #2944 (#2945) 2024-05-12 01:46:28 +00:00
Amir Raminfar e09aeb421b chore: reverts back vue-tsc (#2946) 2024-05-12 01:41:47 +00:00
renovate[bot] 5b12a7ba24 fix(deps): update all non-major dependencies (#2943)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-10 21:43:21 +00:00
Amir Raminfar dc2d892b89 chore: release v6.6.0 2024-05-10 14:37:25 -07:00
Amir Raminfar a991e66436 feat: adds generate subcommand to auto generate users.yml (#2939) 2024-05-10 14:35:49 -07:00
Amir Raminfar 44f68cc482 feat: Shows hosts in cards with their respective stats and updates the container table to filter by host 🥳 (#2932) 2024-05-10 21:35:17 +00:00
renovate[bot] dcb5eb4ccc chore(deps): update all non-major dependencies (#2942)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-09 15:28:33 +00:00
Amir Raminfar 8a2640cd6b chore: send server id by loading system info (#2936) 2024-05-08 01:18:51 +00:00
renovate[bot] d37c1395cb fix(deps): update all non-major dependencies (#2937)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-08 00:13:33 +00:00
Amir Raminfar de23166a22 chore: uses tailwind daisyui progress bar instead 2024-05-07 08:49:40 -07:00
renovate[bot] fdffba179d fix(deps): update dependency unplugin-vue-macros to ^2.9.2 (#2933)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-06 01:02:58 +00:00
renovate[bot] b008354f02 fix(deps): update all non-major dependencies (#2930)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-04 16:37:14 +00:00
Amir Raminfar deaeff73cb feat: sorts homepage every 10 seconds (#2929) 2024-05-02 17:20:13 +00:00
renovate[bot] 88f0a227e7 fix(deps): update dependency vite to v5.2.11 (#2928)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-02 14:47:11 +00:00
renovate[bot] dc993aa816 chore(deps): update all non-major dependencies (#2926)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-01 21:14:22 +00:00
Amir Raminfar cc8e715cc5 feat: downloads visible streams only instead of all (#2924) 2024-04-30 16:40:27 +00:00
renovate[bot] 448341c343 fix(deps): update all non-major dependencies (#2920)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-29 21:27:36 +00:00
renovate[bot] 53b91e662b fix(deps): update all non-major dependencies (#2919)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-28 01:42:13 +00:00
281 changed files with 15928 additions and 8325 deletions
+1
View File
@@ -6,3 +6,4 @@ dist
.git
e2e
docs
internal/agent/pb/
+63
View File
@@ -0,0 +1,63 @@
name: 🐛 Bug Report
labels: ["bug"]
description: |
Use this template if something is not working as expected.
body:
- type: markdown
attributes:
value: |
I get an overwhelming of issues which I don't have time to investigate. Most often, they are due to configuration issues. For this reason, I ask you to help me by providing as much information as possible. For issues with `docker-socket-proxy` I may not be able to help as those are not related to Dozzle.
- type: checkboxes
attributes:
label: 🔍 Check for existing issues
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
options:
- label: Completed
required: true
- type: dropdown
attributes:
label: How is Dozzle deployed?
description: Select the components that you are using.
options:
- Standalone Deployment
- Agents
- Remote Hosts
- Swarm Mode
validations:
required: true
- type: input
attributes:
label: 📦 Dozzle version
description: Run `docker run amir20/dozzle --version` or `curl http://<host>/api/version`.
validations:
required: true
- type: textarea
attributes:
label: ✅ Command used to run Dozzle
description: Please provide the command used to run Dozzle. Provide docker-compose.yml if applicable.
validations:
required: true
- type: textarea
attributes:
label: 🐛 Describe the bug / provide steps to reproduce it
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
id: environment
attributes:
label: 💻 Environment
description: Run `docker system info` and paste the output in the field below..
validations:
required: true
- type: textarea
attributes:
label: 📸 If applicable, add screenshots to help explain your bug
description: Drag issues into the text input below
validations:
required: false
- type: textarea
attributes:
label: 📜 If applicable, attach your Dozzle logs. You many need to enable debug mode. See https://dozzle.dev/guide/debugging.
validations:
required: false
@@ -0,0 +1,20 @@
name: 🚀 Feature Request
labels: ["enhancement"]
description: |
Use this template if you have a feature request or an idea for a new feature.
body:
- type: textarea
attributes:
label: Describe the feature you would like to see
validations:
required: true
- type: textarea
attributes:
label: Describe how you would like to see this feature implemented
validations:
required: false
- type: textarea
attributes:
label: Describe any alternatives you've considered
validations:
required: false
@@ -0,0 +1,52 @@
name: 🚤 Performance Issue
labels: ["performance"]
description: |
Use this template if you are seeing performance issues such as slow loading times or high CPU usage.
body:
- type: checkboxes
attributes:
label: Check for existing issues
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
options:
- label: Completed
required: true
- type: dropdown
attributes:
label: How is Dozzle deployed?
description: Select the components that you are using.
options:
- Standalone Deployment
- Agents
- Remote Hosts
- Swarm Mode
validations:
required: true
- type: input
attributes:
label: Dozzle version
description: Can be found in logs or settings page.
validations:
required: true
- type: textarea
attributes:
label: ✅ Command used to run Dozzle
description: Please provide the command used to run Dozzle. Provide docker-compose.yml if applicable.
validations:
required: true
- type: textarea
attributes:
label: Describe the performance issue you are seeing
validations:
required: true
- type: textarea
attributes:
label: How many containers, hosts, agents, etc. are you using?
description: Any details to reproduce the issue would be helpful. If related to volume of logs, please provide a guesstimate of how many logs are being generated.
validations:
required: true
- type: textarea
attributes:
label: Screenshots, videos, or logs
description: Drag issues into the text input below
validations:
required: false
-33
View File
@@ -1,33 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ""
labels: ""
assignees: amir20
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS, Windows or Mac Os]
- Docker version [e.g. `docker version`'s output]
- Browser & version [e.g. chrome, safari]
- Version [e.g. 1.26.1. Can be found at `<Dozzle-host>/version`]
**If applicable include logs with `--level debug` and browser logs**
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: 💁🏼 Common Issues
url: https://dozzle.dev/guide/faq
about: Check the FAQ for common issues.
-19
View File
@@ -1,19 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ""
labels: enhancement
assignees: amir20
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+1 -1
View File
@@ -1,5 +1,5 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base", "schedule:weekly", "group:allNonMajor", ":automergeMinor"],
"extends": ["config:recommended", "schedule:weekly", "group:allNonMajor", ":automergeMinor"],
"rangeStrategy": "bump"
}
+27 -10
View File
@@ -35,8 +35,14 @@ jobs:
check-latest: true
- name: Checkout code
uses: actions/checkout@v4
- name: Install Protoc
uses: arduino/setup-protoc@v3
- name: Install gRPC and Go
run: |
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
- name: Run Go Tests with Coverage
run: make test SKIP_ASSET=1
run: make test
int-test:
name: Integration Tests
timeout-minutes: 60
@@ -58,8 +64,12 @@ jobs:
run: pnpm install
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Writing certs to file
run: |
echo "${{ secrets.TTL_KEY }}" > shared_key.pem
echo "${{ secrets.TTL_CERT }}" > shared_cert.pem
- name: Build
uses: docker/bake-action@v4
uses: docker/bake-action@v5
with:
load: true
set: |
@@ -78,22 +88,29 @@ jobs:
name: Release
runs-on: ubuntu-latest
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
- name: Login to DockerHub
uses: docker/login-action@v3.3.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Checkout
uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: amir20/dozzle
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0
- name: Login to DockerHub
uses: docker/login-action@v3.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Writing certs to file
run: |
echo "${{ secrets.TTL_KEY }}" > shared_key.pem
echo "${{ secrets.TTL_CERT }}" > shared_cert.pem
- name: Build and push
uses: docker/build-push-action@v5.3.0
uses: docker/build-push-action@v6.9.0
with:
push: true
context: .
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }}
build-args: TAG=${{ steps.meta.outputs.version }}
+15 -8
View File
@@ -12,21 +12,28 @@ jobs:
runs-on: ubuntu-latest
if: ${{ !github.event.repository.fork && !github.event.pull_request.head.repo.fork && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == 'amir20/dozzle') }}
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
- name: Login to DockerHub
uses: docker/login-action@v3.3.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Checkout
uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: amir20/dozzle
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0
- name: Login to DockerHub
uses: docker/login-action@v3.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Writing certs to file
run: |
echo "${{ secrets.TTL_KEY }}" > shared_key.pem
echo "${{ secrets.TTL_CERT }}" > shared_cert.pem
- name: Build and push
uses: docker/build-push-action@v5.3.0
uses: docker/build-push-action@v6.9.0
with:
context: .
push: true
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }}
+22 -4
View File
@@ -54,16 +54,32 @@ jobs:
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: 1.22.x
go-version: "1.23"
check-latest: true
- name: Checkout code
uses: actions/checkout@v4
- name: Install Protoc
uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install gRPC and Go
run: |
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
- name: Run Go Tests with Coverage
run: make test SKIP_ASSET=1
run: make test
staticcheck:
runs-on: ubuntu-latest
name: Go Staticcheck
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Generate dependencies
run: make fake_assets shared_key.pem shared_cert.pem
- name: Stactic checker
uses: dominikh/staticcheck-action@v1.3.1
with:
install-go: false
version: "latest"
int-test:
name: Integration Tests
timeout-minutes: 60
@@ -85,8 +101,10 @@ jobs:
run: pnpm install
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Generate certs
run: make shared_key.pem shared_cert.pem
- name: Build
uses: docker/bake-action@v4
uses: docker/bake-action@v5
with:
load: true
set: |
+2
View File
@@ -12,3 +12,5 @@ coverage.out
/test-results/
/playwright-report/
/playwright/.cache/
*.pem
*.csr
+1
View File
@@ -1,3 +1,4 @@
shared-workspace-lockfile=false
include-workspace-root=true
ignore-workspace-root-check=true
+1
View File
@@ -0,0 +1 @@
-r '\.(go)$' -R 'node_modules' -G '\*\_test.go' -s -- go run -race main.go --level debug agent
View File
+16
View File
@@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Debug test",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/main.go",
"args": ["test"]
}
]
}
+3 -2
View File
@@ -1,6 +1,7 @@
{
"i18n-ally.localesPaths": ["locales"],
"i18n-ally.keystyle": "nested",
"cSpell.words": ["healthcheck"],
"editor.formatOnSave": true
"cSpell.words": ["healthcheck", "orderedmap", "Retriable", "stdcopy", "Warnf"],
"editor.formatOnSave": true,
"i18n-ally.extract.autoDetect": true
}
+16 -8
View File
@@ -1,5 +1,5 @@
# Build assets
FROM --platform=$BUILDPLATFORM node:21.7.3-alpine as node
FROM --platform=$BUILDPLATFORM node:23.1.0-alpine AS node
RUN corepack enable
@@ -22,9 +22,13 @@ COPY public ./public
# Build assets
RUN pnpm build
FROM --platform=$BUILDPLATFORM golang:1.22.2-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.23.2-alpine AS builder
RUN apk add --no-cache ca-certificates && mkdir /dozzle
# install gRPC dependencies
RUN apk add --no-cache ca-certificates protoc protobuf-dev\
&& mkdir /dozzle \
&& go install google.golang.org/protobuf/cmd/protoc-gen-go@latest \
&& go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
WORKDIR /dozzle
@@ -32,25 +36,29 @@ WORKDIR /dozzle
COPY go.* ./
RUN go mod download
# Copy assets built with node
COPY --from=node /build/dist ./dist
# Copy all other files
COPY internal ./internal
COPY main.go ./
COPY protos ./protos
COPY shared_key.pem shared_cert.pem ./
# Copy assets built with node
COPY --from=node /build/dist ./dist
# Args
ARG TAG=dev
ARG TARGETOS TARGETARCH
# Generate protos
RUN go generate
# Build binary
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=$TAG" -o dozzle
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags "-s -w -X github.com/amir20/dozzle/internal/support/cli.Version=$TAG" -o dozzle
RUN 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
+43 -7
View File
@@ -1,7 +1,14 @@
PROTO_DIR := protos
GEN_DIR := internal/agent/pb
PROTO_FILES := $(wildcard $(PROTO_DIR)/*.proto)
GEN_FILES := $(patsubst $(PROTO_DIR)/%.proto,$(GEN_DIR)/%.pb.go,$(PROTO_FILES))
.PHONY: clean
clean:
@rm -rf dist
@go clean -i
@rm -f shared_key.pem shared_cert.pem
@rm -f $(GEN_DIR)/*.pb.go
.PHONY: dist
dist:
@@ -14,21 +21,50 @@ fake_assets:
@echo "assets build was skipped" > dist/index.html
.PHONY: test
test: fake_assets
go test -cover -race ./...
test: fake_assets generate
go test -cover -race -count 1 -timeout 40s ./...
.PHONY: build
build: dist
CGO_ENABLED=0 go build -ldflags "-s -w"
build: dist generate
CGO_ENABLED=0 go build -ldflags "-s -w -X github.com/amir20/dozzle/internal/support/cli.Version=local"
.PHONY: docker
docker:
@docker build -t amir20/dozzle .
docker: shared_key.pem shared_cert.pem
@docker build --build-arg TAG=local -t amir20/dozzle .
generate: shared_key.pem shared_cert.pem $(GEN_FILES)
.PHONY: dev
dev:
dev: generate fake_assets
pnpm dev
.PHONY: int
int:
docker compose up --build --force-recreate --exit-code-from playwright
shared_key.pem:
@openssl genpkey -algorithm RSA -out shared_key.pem -pkeyopt rsa_keygen_bits:2048
shared_cert.pem: shared_key.pem
@openssl req -new -key shared_key.pem -out shared_request.csr -subj "/C=US/ST=California/L=San Francisco/O=Dozzle"
@openssl x509 -req -in shared_request.csr -signkey shared_key.pem -out shared_cert.pem -days 365
@rm shared_request.csr
$(GEN_DIR)/%.pb.go: $(PROTO_DIR)/%.proto
@go generate
.PHONY: push
push: docker
@docker tag amir20/dozzle:latest amir20/dozzle:local-test
@docker push amir20/dozzle:local-test
tools:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go install github.com/cespare/reflex@latest
run: docker
docker run -it --rm -p 8080:8080 -v /var/run/docker.sock:/var/run/docker.sock amir20/dozzle:latest
preview: build
pnpm preview
+48 -29
View File
@@ -2,9 +2,9 @@
Dozzle is a small lightweight application with a web based interface to monitor Docker logs. It doesnt store any log files. It is for live monitoring of your container logs only.
https://user-images.githubusercontent.com/260667/227634771-9ebbe381-16a8-465a-b28a-450c5cd20c94.mp4
https://github.com/user-attachments/assets/66a7b4b2-d6c9-4fca-ab04-aef6cd7c0c31
[![Go Report Card](https://goreportcard.com/badge/github.com/amir20/dozzle)](https://goreportcard.com/report/github.com/amir20/dozzle)
[![Docker Image Size (latest by date)](https://img.shields.io/docker/image-size/amir20/dozzle)](https://hub.docker.com/r/amir20/dozzle/)
[![Docker Pulls](https://img.shields.io/docker/pulls/amir20/dozzle.svg)](https://hub.docker.com/r/amir20/dozzle/)
[![Docker Version](https://img.shields.io/docker/v/amir20/dozzle?sort=semver)](https://hub.docker.com/r/amir20/dozzle/)
![Test](https://github.com/amir20/dozzle/workflows/Test/badge.svg)
@@ -13,17 +13,20 @@ https://user-images.githubusercontent.com/260667/227634771-9ebbe381-16a8-465a-b2
- Intelligent fuzzy search for container names 🤖
- Search logs using regex 🔦
- Search logs using [SQL queries](https://dozzle.dev/guide/sql-engine) 📊
- Small memory footprint 🏎
- Split screen for viewing multiple logs
- Download logs easily
- Live stats with memory and CPU usage
- Multi-user authentication with support for proxy forward authorization 🚨
- Multi-user [authentication](https://dozzle.dev/guide/authentication) with support for proxy forward authorization 🚨
- [Swarm](https://dozzle.dev/guide/swarm-mode) mode support 🐳
- [Agent](https://dozzle.dev/guide/agent) mode for monitoring multiple Docker hosts 🕵️‍♂️
- Dark mode 🌙
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.
## Getting Started
Dozzle is a small container (4 MB compressed). Pull the latest release with:
Dozzle is a small container (7 MB compressed). Pull the latest release with:
$ docker pull amir20/dozzle:latest
@@ -31,24 +34,39 @@ Dozzle is a small container (4 MB compressed). Pull the latest release with:
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:ro -p 8888:8080 amir20/dozzle:latest
$ docker run --name dozzle -d --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir20/dozzle:latest
Dozzle will be available at [http://localhost:8888/](http://localhost:8888/).
Dozzle will be available at [http://localhost:8080/](http://localhost:8080/).
Here is the Docker Compose file:
version: "3"
services:
dozzle:
container_name: dozzle
image: amir20/dozzle:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /var/run/docker.sock:/var/run/docker.sock
ports:
- 8888:8080
- 8080:8080
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).
## Swarm Mode
Dozzle works with Docker Swarm mode. You can run Dozzle as a global service with:
$ docker service create --name dozzle --env DOZZLE_MODE=swarm --mode global --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock -p 8080:8080 amir20/dozzle:latest
See the [Swarm Mode](https://dozzle.dev/guide/swarm-mode) documentation for more details.
## Agent Mode
Dozzle can be used to monitor multiple Docker hosts. You can run Dozzle in agent mode with:
$ docker run -v /var/run/docker.sock:/var/run/docker.sock -p 7007:7007 amir20/dozzle:latest agent
See the [Agent Mode](https://dozzle.dev/guide/agent-mode) documentation for more details.
## Technical Details
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/).
@@ -76,7 +94,7 @@ If it's not enabled please follow [this tutorial](https://github.com/containers/
Once you have the podman remote socket you can run Dozzle on podman.
```
podman run --volume=/run/user/1000/podman/podman.sock:/var/run/docker.sock:ro -d -p 8888:8080 amir20/dozzle:latest
podman run --volume=/run/user/1000/podman/podman.sock:/var/run/docker.sock -d -p 8080:8080 amir20/dozzle:latest
```
## Security
@@ -93,21 +111,22 @@ If you do not want to be tracked at all, see the `--no-analytics` flag below.
Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can use the CLI flags or environment variables. The table below outlines all supported options and their respective env vars.
| Flag | Env Variable | Default |
| --------------------------- | -------------------------------- | -------------- |
| `--addr` | `DOZZLE_ADDR` | `:8080` |
| `--base` | `DOZZLE_BASE` | `/` |
| `--hostname` | `DOZZLE_HOSTNAME` | `""` |
| `--level` | `DOZZLE_LEVEL` | `info` |
| `--auth-provider` | `DOZZLE_AUTH_PROVIDER` | `none` |
| `--auth-header-user` | `DOZZLE_AUTH_HEADER_USER` | `Remote-User` |
| `--auth-header-email` | `DOZZLE_AUTH_HEADER_EMAIL` | `Remote-Email` |
| `--auth-header-name` | `DOZZLE_AUTH_HEADER_NAME` | `Remote-Name` |
| `--enable-actions` | `DOZZLE_ENABLE_ACTIONS` | false |
| `--wait-for-docker-seconds` | `DOZZLE_WAIT_FOR_DOCKER_SECONDS` | 0 |
| `--filter` | `DOZZLE_FILTER` | `""` |
| `--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 |
| `--filter` | `DOZZLE_FILTER` | `""` |
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
| `--mode` | `DOZZLE_MODE` | `server` |
| `--remote-host` | `DOZZLE_REMOTE_HOST` | |
| `--remote-agent` | `DOZZLE_REMOTE_AGENT` | |
## Support
@@ -125,10 +144,10 @@ There are many ways you can support Dozzle:
## Building
To Build and test locally:
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 install github.com/cespare/reflex@latest`.
3. Install tools with `make tools`.
4. Install node modules `pnpm install`.
5. Do `pnpm dev`
5. Run `make dev` to start a development server with hot reload.
+51 -380
View File
@@ -3,18 +3,13 @@
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const $: typeof import('vue/macros')['$']
const $$: typeof import('vue/macros')['$$']
const $computed: typeof import('vue/macros')['$computed']
const $customRef: typeof import('vue/macros')['$customRef']
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('./stores/settings')['DEFAULT_SETTINGS']
const EffectScope: typeof import('vue')['EffectScope']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const allLevels: typeof import('./composable/logContext')['allLevels']
const arrayEquals: typeof import('./utils/index')['arrayEquals']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
@@ -27,10 +22,10 @@ declare global {
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']
const createDrawer: typeof import('./composable/drawer')['createDrawer']
const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
@@ -47,10 +42,12 @@ declare global {
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const drawerContext: typeof import('./composable/drawer')['drawerContext']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
const flattenJSON: typeof import('./utils/index')['flattenJSON']
const flattenJSONToMap: typeof import('./utils/index')['flattenJSONToMap']
const formatBytes: typeof import('./utils/index')['formatBytes']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
@@ -58,6 +55,7 @@ declare global {
const getDeep: typeof import('./utils/index')['getDeep']
const globalShowPopup: typeof import('./composable/popup')['globalShowPopup']
const h: typeof import('vue')['h']
const hashCode: typeof import('./utils/index')['hashCode']
const hourStyle: typeof import('./stores/settings')['hourStyle']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
@@ -71,7 +69,7 @@ declare global {
const isRef: typeof import('vue')['isRef']
const lightTheme: typeof import('./stores/settings')['lightTheme']
const locale: typeof import('./stores/settings')['locale']
const logSearchContext: typeof import('./composable/logSearchContext')['logSearchContext']
const loggingContextKey: typeof import('./composable/logContext')['loggingContextKey']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
@@ -100,12 +98,14 @@ declare global {
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const persistentVisibleKeys: typeof import('./composable/storage')['persistentVisibleKeys']
const persistentVisibleKeysForContainer: typeof import('./composable/storage')['persistentVisibleKeysForContainer']
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 provideLoggingContext: typeof import('./composable/logContext')['provideLoggingContext']
const provideScrollContext: typeof import('./composable/scrollContext')['provideScrollContext']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
@@ -122,6 +122,7 @@ declare global {
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const scrollContextKey: typeof import('./composable/scrollContext')['scrollContextKey']
const search: typeof import('./stores/settings')['search']
const sessionHost: typeof import('./composable/storage')['sessionHost']
const setActivePinia: typeof import('pinia')['setActivePinia']
@@ -188,8 +189,8 @@ declare global {
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 useContainerStream: typeof import('./composable/eventStreams')['useContainerStream']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar']
@@ -208,7 +209,9 @@ declare global {
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
const useDraggable: typeof import('@vueuse/core')['useDraggable']
const useDrawer: typeof import('./composable/drawer')['useDrawer']
const useDropZone: typeof import('@vueuse/core')['useDropZone']
const useDuckDB: typeof import('./composable/duckdb')['useDuckDB']
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
const useElementHover: typeof import('@vueuse/core')['useElementHover']
@@ -229,9 +232,11 @@ declare global {
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useGroupedStream: typeof import('./composable/eventStreams')['useGroupedStream']
const useHead: typeof import('@vueuse/head')['useHead']
const useHosts: typeof import('./stores/hosts')['useHosts']
const useI18n: typeof import('vue-i18n')['useI18n']
const useId: typeof import('vue')['useId']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
@@ -240,16 +245,16 @@ declare global {
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLink: typeof import('vue-router')['useLink']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useLogSearchContext: typeof import('./composable/logSearchContext')['useLogSearchContext']
const useLogStream: typeof import('./composable/eventsource')['useLogStream']
const useLoggingContext: typeof import('./composable/logContext')['useLoggingContext']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useMergedStream: typeof import('./composable/eventStreams')['useMergedStream']
const useModel: typeof import('vue')['useModel']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
@@ -266,6 +271,7 @@ declare global {
const useParentElement: typeof import('@vueuse/core')['useParentElement']
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePinnedLogsStore: typeof import('./stores/pinned')['usePinnedLogsStore']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
@@ -286,9 +292,11 @@ declare global {
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollContext: typeof import('./composable/scrollContext')['useScrollContext']
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSearchFilter: typeof import('./composable/search')['useSearchFilter']
const useSeoMeta: typeof import('@vueuse/head')['useSeoMeta']
const useServiceStream: typeof import('./composable/eventStreams')['useServiceStream']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useSimpleRefHistory: typeof import('./utils/index')['useSimpleRefHistory']
@@ -296,12 +304,15 @@ declare global {
const useSorted: typeof import('@vueuse/core')['useSorted']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useStackStream: typeof import('./composable/eventStreams')['useStackStream']
const useStepper: typeof import('@vueuse/core')['useStepper']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwarmStore: typeof import('./stores/swarm')['useSwarmStore']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
@@ -364,16 +375,10 @@ import { UnwrapRef } from 'vue'
declare module 'vue' {
interface GlobalComponents {}
interface ComponentCustomProperties {
readonly $$: UnwrapRef<typeof import('vue/macros')['$$']>
readonly $: UnwrapRef<typeof import('vue/macros')['$']>
readonly $computed: UnwrapRef<typeof import('vue/macros')['$computed']>
readonly $customRef: UnwrapRef<typeof import('vue/macros')['$customRef']>
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('./stores/settings')['DEFAULT_SETTINGS']>
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly allLevels: UnwrapRef<typeof import('./composable/logContext')['allLevels']>
readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
@@ -386,10 +391,10 @@ declare module 'vue' {
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']>
readonly createDrawer: UnwrapRef<typeof import('./composable/drawer')['createDrawer']>
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
@@ -406,10 +411,12 @@ declare module 'vue' {
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
readonly drawerContext: UnwrapRef<typeof import('./composable/drawer')['drawerContext']>
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly flattenJSON: UnwrapRef<typeof import('./utils/index')['flattenJSON']>
readonly flattenJSONToMap: UnwrapRef<typeof import('./utils/index')['flattenJSONToMap']>
readonly formatBytes: UnwrapRef<typeof import('./utils/index')['formatBytes']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
@@ -417,6 +424,7 @@ declare module 'vue' {
readonly getDeep: UnwrapRef<typeof import('./utils/index')['getDeep']>
readonly globalShowPopup: UnwrapRef<typeof import('./composable/popup')['globalShowPopup']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly hashCode: UnwrapRef<typeof import('./utils/index')['hashCode']>
readonly hourStyle: UnwrapRef<typeof import('./stores/settings')['hourStyle']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
@@ -430,6 +438,7 @@ declare module 'vue' {
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly lightTheme: UnwrapRef<typeof import('./stores/settings')['lightTheme']>
readonly locale: UnwrapRef<typeof import('./stores/settings')['locale']>
readonly loggingContextKey: UnwrapRef<typeof import('./composable/logContext')['loggingContextKey']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
@@ -458,12 +467,14 @@ declare module 'vue' {
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly persistentVisibleKeys: UnwrapRef<typeof import('./composable/storage')['persistentVisibleKeys']>
readonly persistentVisibleKeysForContainer: UnwrapRef<typeof import('./composable/storage')['persistentVisibleKeysForContainer']>
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 provideLoggingContext: UnwrapRef<typeof import('./composable/logContext')['provideLoggingContext']>
readonly provideScrollContext: UnwrapRef<typeof import('./composable/scrollContext')['provideScrollContext']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
@@ -480,6 +491,7 @@ 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 scrollContextKey: UnwrapRef<typeof import('./composable/scrollContext')['scrollContextKey']>
readonly search: UnwrapRef<typeof import('./stores/settings')['search']>
readonly sessionHost: UnwrapRef<typeof import('./composable/storage')['sessionHost']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
@@ -546,8 +558,8 @@ declare module 'vue' {
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 useContainerStream: UnwrapRef<typeof import('./composable/eventStreams')['useContainerStream']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
@@ -566,7 +578,9 @@ declare module 'vue' {
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
readonly useDrawer: UnwrapRef<typeof import('./composable/drawer')['useDrawer']>
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
readonly useDuckDB: UnwrapRef<typeof import('./composable/duckdb')['useDuckDB']>
readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
@@ -587,9 +601,11 @@ declare module 'vue' {
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useGroupedStream: UnwrapRef<typeof import('./composable/eventStreams')['useGroupedStream']>
readonly useHead: UnwrapRef<typeof import('@vueuse/head')['useHead']>
readonly useHosts: UnwrapRef<typeof import('./stores/hosts')['useHosts']>
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
readonly useId: UnwrapRef<typeof import('vue')['useId']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
@@ -598,367 +614,16 @@ declare module 'vue' {
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
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 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']>
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useProfileStorage: UnwrapRef<typeof import('./composable/profileStorage')['useProfileStorage']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useReleases: UnwrapRef<typeof import('./stores/releases')['useReleases']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
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('./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']>
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
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']>
readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
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('./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']>
readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>
readonly watch: UnwrapRef<typeof import('vue')['watch']>
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']>
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')['$']>
readonly $computed: UnwrapRef<typeof import('vue/macros')['$computed']>
readonly $customRef: UnwrapRef<typeof import('vue/macros')['$customRef']>
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('./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 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']>
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
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']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly flattenJSON: UnwrapRef<typeof import('./utils/index')['flattenJSON']>
readonly formatBytes: UnwrapRef<typeof import('./utils/index')['formatBytes']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
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('./composable/popup')['globalShowPopup']>
readonly h: UnwrapRef<typeof import('vue')['h']>
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('./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('./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']>
readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
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('./stores/settings')['menuWidth']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
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('./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']>
readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']>
readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
readonly ref: UnwrapRef<typeof import('vue')['ref']>
readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly 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('./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('./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']>
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']>
readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']>
readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']>
readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']>
readonly unref: UnwrapRef<typeof import('vue')['unref']>
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
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']>
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']>
readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']>
readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']>
readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']>
readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']>
readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']>
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
readonly useExponentialMovingAverage: UnwrapRef<typeof import('./utils/index')['useExponentialMovingAverage']>
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']>
readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']>
readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useHead: UnwrapRef<typeof import('@vueuse/head')['useHead']>
readonly useHosts: UnwrapRef<typeof import('./stores/hosts')['useHosts']>
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
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 useLogSearchContext: UnwrapRef<typeof import('./composable/logSearchContext')['useLogSearchContext']>
readonly useLogStream: UnwrapRef<typeof import('./composable/eventsource')['useLogStream']>
readonly useLoggingContext: UnwrapRef<typeof import('./composable/logContext')['useLoggingContext']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
readonly useMergedStream: UnwrapRef<typeof import('./composable/eventStreams')['useMergedStream']>
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
@@ -975,6 +640,7 @@ declare module '@vue/runtime-core' {
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
readonly usePinnedLogsStore: UnwrapRef<typeof import('./stores/pinned')['usePinnedLogsStore']>
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
@@ -995,9 +661,11 @@ declare module '@vue/runtime-core' {
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollContext: UnwrapRef<typeof import('./composable/scrollContext')['useScrollContext']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSearchFilter: UnwrapRef<typeof import('./composable/search')['useSearchFilter']>
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']>
readonly useServiceStream: UnwrapRef<typeof import('./composable/eventStreams')['useServiceStream']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSimpleRefHistory: UnwrapRef<typeof import('./utils/index')['useSimpleRefHistory']>
@@ -1005,12 +673,15 @@ declare module '@vue/runtime-core' {
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
readonly useStackStream: UnwrapRef<typeof import('./composable/eventStreams')['useStackStream']>
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwarmStore: UnwrapRef<typeof import('./stores/swarm')['useSwarmStore']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
+42 -16
View File
@@ -1,13 +1,12 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
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']
@@ -15,7 +14,6 @@ declare module 'vue' {
'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']
@@ -25,35 +23,38 @@ declare module 'vue' {
'Cil:columns': typeof import('~icons/cil/columns')['default']
'Cil:xCircle': typeof import('~icons/cil/x-circle')['default']
ComplexLogItem: typeof import('./components/LogViewer/ComplexLogItem.vue')['default']
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']
ContainerActionsToolbar: typeof import('./components/ContainerViewer/ContainerActionsToolbar.vue')['default']
ContainerEventLogItem: typeof import('./components/LogViewer/ContainerEventLogItem.vue')['default']
ContainerHealth: typeof import('./components/ContainerViewer/ContainerHealth.vue')['default']
ContainerLog: typeof import('./components/ContainerViewer/ContainerLog.vue')['default']
ContainerName: typeof import('./components/LogViewer/ContainerName.vue')['default']
ContainerPopup: typeof import('./components/ContainerPopup.vue')['default']
ContainerTable: typeof import('./components/ContainerTable.vue')['default']
ContainerTitle: typeof import('./components/LogViewer/ContainerTitle.vue')['default']
ContainerTitle: typeof import('./components/ContainerViewer/ContainerTitle.vue')['default']
DateTime: typeof import('./components/common/DateTime.vue')['default']
DistanceTime: typeof import('./components/common/DistanceTime.vue')['default']
DockerEventLogItem: typeof import('./components/LogViewer/DockerEventLogItem.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']
EventSource: typeof import('./components/LogViewer/EventSource.vue')['default']
FuzzySearchModal: typeof import('./components/FuzzySearchModal.vue')['default']
GroupedLog: typeof import('./components/GroupedViewer/GroupedLog.vue')['default']
HostIcon: typeof import('./components/common/HostIcon.vue')['default']
HostList: typeof import('./components/HostList.vue')['default']
HostMenu: typeof import('./components/HostMenu.vue')['default']
'Ic:sharpKeyboardReturn': typeof import('~icons/ic/sharp-keyboard-return')['default']
InfiniteLoader: typeof import('./components/InfiniteLoader.vue')['default']
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']
LogAnalytics: typeof import('./components/LogViewer/LogAnalytics.vue')['default']
LogDate: typeof import('./components/LogViewer/LogDate.vue')['default']
LogEventSource: typeof import('./components/LogViewer/LogEventSource.vue')['default']
LogDetails: typeof import('./components/LogViewer/LogDetails.vue')['default']
LogLevel: typeof import('./components/LogViewer/LogLevel.vue')['default']
LogList: typeof import('./components/LogViewer/LogList.vue')['default']
LogMessageActions: typeof import('./components/LogViewer/LogMessageActions.vue')['default']
LogStd: typeof import('./components/LogViewer/LogStd.vue')['default']
LogViewer: typeof import('./components/LogViewer/LogViewer.vue')['default']
LogViewerWithSource: typeof import('./components/LogViewer/LogViewerWithSource.vue')['default']
'MaterialSymbols:collapseAllRounded': typeof import('~icons/material-symbols/collapse-all-rounded')['default']
'MaterialSymbols:expandAllRounded': typeof import('~icons/material-symbols/expand-all-rounded')['default']
'Mdi:account': typeof import('~icons/mdi/account')['default']
'Mdi:announcement': typeof import('~icons/mdi/announcement')['default']
'Mdi:arrowUp': typeof import('~icons/mdi/arrow-up')['default']
'Mdi:check': typeof import('~icons/mdi/check')['default']
@@ -62,17 +63,35 @@ declare module 'vue' {
'Mdi:chevronRight': typeof import('~icons/mdi/chevron-right')['default']
'Mdi:close': typeof import('~icons/mdi/close')['default']
'Mdi:cog': typeof import('~icons/mdi/cog')['default']
'Mdi:contentCopy': typeof import('~icons/mdi/content-copy')['default']
'Mdi:docker': typeof import('~icons/mdi/docker')['default']
'Mdi:gauge': typeof import('~icons/mdi/gauge')['default']
'Mdi:hamburgerMenu': typeof import('~icons/mdi/hamburger-menu')['default']
'Mdi:hexagonMultiple': typeof import('~icons/mdi/hexagon-multiple')['default']
'Mdi:key': typeof import('~icons/mdi/key')['default']
'Mdi:keyboardEsc': typeof import('~icons/mdi/keyboard-esc')['default']
'Mdi:magnify': typeof import('~icons/mdi/magnify')['default']
'Mdi:satelliteVariant': typeof import('~icons/mdi/satellite-variant')['default']
MobileMenu: typeof import('./components/common/MobileMenu.vue')['default']
MultiContainerActionToolbar: typeof import('./components/LogViewer/MultiContainerActionToolbar.vue')['default']
MultiContainerLog: typeof import('./components/MultiContainerViewer/MultiContainerLog.vue')['default']
MultiContainerStat: typeof import('./components/LogViewer/MultiContainerStat.vue')['default']
'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:arrowsMerge': typeof import('~icons/ph/arrows-merge')['default']
'Ph:boundingBoxFill': typeof import('~icons/ph/bounding-box-fill')['default']
'Ph:circlesFour': typeof import('~icons/ph/circles-four')['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']
'Ph:cpu': typeof import('~icons/ph/cpu')['default']
'Ph:fileSql': typeof import('~icons/ph/file-sql')['default']
'Ph:globeSimple': typeof import('~icons/ph/globe-simple')['default']
'Ph:memory': typeof import('~icons/ph/memory')['default']
'Ph:stack': typeof import('~icons/ph/stack')['default']
'Ph:stackSimple': typeof import('~icons/ph/stack-simple')['default']
Popup: typeof import('./components/Popup.vue')['default']
Releases: typeof import('./components/Releases.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
@@ -80,15 +99,22 @@ declare module 'vue' {
ScrollableView: typeof import('./components/ScrollableView.vue')['default']
ScrollProgress: typeof import('./components/ScrollProgress.vue')['default']
Search: typeof import('./components/Search.vue')['default']
ServiceLog: typeof import('./components/ServiceViewer/ServiceLog.vue')['default']
SideDrawer: typeof import('./components/common/SideDrawer.vue')['default']
SideMenu: typeof import('./components/SideMenu.vue')['default']
SidePanel: typeof import('./components/SidePanel.vue')['default']
SimpleLogItem: typeof import('./components/LogViewer/SimpleLogItem.vue')['default']
SkippedEntriesLogItem: typeof import('./components/LogViewer/SkippedEntriesLogItem.vue')['default']
SlideTransition: typeof import('./components/common/SlideTransition.vue')['default']
SQLTable: typeof import('./components/LogViewer/SQLTable.vue')['default']
StackLog: typeof import('./components/StackViewer/StackLog.vue')['default']
StatMonitor: typeof import('./components/LogViewer/StatMonitor.vue')['default']
StatSparkline: typeof import('./components/LogViewer/StatSparkline.vue')['default']
SwarmMenu: typeof import('./components/SwarmMenu.vue')['default']
Tag: typeof import('./components/common/Tag.vue')['default']
TimedButton: typeof import('./components/common/TimedButton.vue')['default']
Toggle: typeof import('./components/common/Toggle.vue')['default']
ViewerWithSource: typeof import('./components/LogViewer/ViewerWithSource.vue')['default']
ZigZag: typeof import('./components/LogViewer/ZigZag.vue')['default']
}
}
-20
View File
@@ -1,20 +0,0 @@
<template>
<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>
</template>
<script lang="ts" setup>
const { value } = defineProps<{ value: number }>();
const minValue = computed(() => Math.min(value, 1));
</script>
<style scoped>
.bar {
transform: scaleX(v-bind(minValue));
}
</style>
@@ -2,7 +2,7 @@
<div>
<span class="font-light capitalize"> RUNNING </span>
<span class="font-semibold">
<distance-time :date="container.created" strict :suffix="false"></distance-time>
<DistanceTime :date="container.created" strict :suffix="false" />
</span>
</div>
<div>
+74 -27
View File
@@ -1,13 +1,51 @@
!
<template>
<div class="text-right" v-if="containers.length > pageSizes[0]">
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 }))"
/>
<div class="flex flex-row">
<div v-if="Object.keys(hosts).length > 1" class="flex-1">
<div role="tablist" class="tabs-boxed tabs block" v-if="Object.keys(hosts).length < 4">
<input
type="radio"
name="host"
role="tab"
class="tab !rounded"
aria-label="Show All"
v-model="selectedHost"
:value="null"
/>
<input
type="radio"
name="host"
role="tab"
class="tab !rounded"
:aria-label="host.name"
v-for="host in hosts"
:value="host.id"
:key="host.id"
v-model="selectedHost"
/>
</div>
<DropdownMenu
class="btn-sm"
v-model="selectedHost"
:options="[
{ label: 'Show All', value: null },
...Object.values(hosts).map((host) => ({ label: host.name, value: host.id })),
]"
v-else
/>
</div>
<div class="flex-1 text-right" v-show="containers.length > pageSizes[0]">
{{ $t("label.per-page") }}
<DropdownMenu
class="dropdown-left btn-xs md:btn-sm"
v-model="perPage"
:options="pageSizes.map((i) => ({ label: i.toLocaleString(), value: i }))"
/>
</div>
</div>
<table class="table table-lg bg-base">
<thead>
<tr :data-direction="direction > 0 ? 'asc' : 'desc'">
@@ -30,7 +68,7 @@
<tbody>
<tr v-for="container in paginated" :key="container.id">
<td v-if="isVisible('name')">
<router-link :to="{ name: 'container-id', params: { id: container.id } }" :title="container.name">
<router-link :to="{ name: '/container/[id]', params: { id: container.id } }" :title="container.name">
{{ container.name }}
</router-link>
</td>
@@ -40,28 +78,34 @@
<distance-time :date="container.created" strict :suffix="false"></distance-time>
</td>
<td v-if="isVisible('cpu')">
<bar-chart :value="container.movingAverage.cpu / 100">
{{ (container.movingAverage.cpu / 100).toLocaleString(undefined, { style: "percent" }) }}
</bar-chart>
<div class="flex flex-row items-center gap-1">
<progress
class="progress progress-primary"
:value="Math.min(container.movingAverage.cpu, 100)"
:max="100"
></progress>
<span class="text-sm">{{ container.movingAverage.cpu.toFixed(0) }}%</span>
</div>
</td>
<td v-if="isVisible('mem')">
<bar-chart :value="container.movingAverage.memory / 100">
{{ (container.movingAverage.memory / 100).toLocaleString(undefined, { style: "percent" }) }}
</bar-chart>
<div class="flex flex-row items-center gap-1">
<progress class="progress progress-primary" :value="container.movingAverage.memory" max="100"></progress>
<span class="text-sm">{{ container.movingAverage.memory.toFixed(0) }}%</span>
</div>
</td>
</tr>
</tbody>
</table>
<div class="p-4 text-center">
<nav class="join" v-if="isPaginated">
<button
<input
class="btn btn-square join-item"
type="radio"
v-model="currentPage"
:aria-label="`${i}`"
:value="i"
v-for="i in totalPages"
class="btn join-item"
:class="{ 'btn-primary': i === currentPage }"
@click="currentPage = i"
>
{{ i }}
</button>
/>
</nav>
</div>
</template>
@@ -70,6 +114,9 @@
import { Container } from "@/models/Container";
import { toRefs } from "@vueuse/core";
const { hosts } = useHosts();
const selectedHost = ref(null);
const fields = {
name: {
label: "label.container-name",
@@ -116,13 +163,13 @@ const storage = useStorage<{ column: keys; direction: 1 | -1 }>("DOZZLE_TABLE_CO
direction: -1,
});
const { column: sortField, direction } = toRefs(storage);
const counter = useInterval(10000);
const filteredContainers = computed(() =>
containers.filter((c) => selectedHost.value === null || c.host === selectedHost.value),
);
const sortedContainers = computedWithControl(
() => [containers.length, sortField.value, direction.value],
() => {
return containers.sort((a, b) => {
return fields[sortField.value].sortFunc(a, b);
});
},
() => [filteredContainers.value.length, sortField.value, direction.value, counter.value],
() => filteredContainers.value.sort((a, b) => fields[sortField.value].sortFunc(a, b)),
);
const totalPages = computed(() => Math.ceil(sortedContainers.value.length / perPage.value));
@@ -0,0 +1,205 @@
<template>
<div class="dropdown dropdown-end dropdown-hover">
<label tabindex="0" class="btn btn-ghost btn-sm w-10 gap-0.5 px-2">
<carbon:circle-solid class="w-2.5 text-red" v-if="streamConfig.stderr" />
<carbon:circle-solid class="w-2.5 text-blue" v-if="streamConfig.stdout" />
</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") }}
<KeyShortcut char="k" :modifiers="['shift', 'meta']" />
</a>
</li>
<li>
<a :href="downloadUrl" download> <octicon:download-24 /> {{ $t("toolbar.download") }} </a>
</li>
<li>
<a @click.prevent="showSearch = true">
<mdi:magnify /> {{ $t("toolbar.search") }}
<KeyShortcut char="f" />
</a>
</li>
<li v-if="hasComplexLogs">
<a @click.prevent="showDrawer(LogAnalytics, { container }, 'lg')">
<ph:file-sql /> SQL Analytics
<KeyShortcut char="f" :modifiers="['shift', 'meta']" />
</a>
</li>
<li class="line"></li>
<li>
<details>
<summary>
<div class="flex w-4">
<carbon:circle-solid class="w-2.5 text-red" v-if="streamConfig.stderr" />
<carbon:circle-solid class="w-2.5 text-blue" v-if="streamConfig.stdout" />
</div>
Streams
</summary>
<ul class="menu">
<li>
<a
@click="
streamConfig.stdout = true;
streamConfig.stderr = true;
"
>
<mdi:check class="w-4" v-if="streamConfig.stdout == true && streamConfig.stderr == true" />
<div v-else class="w-4"></div>
{{ $t("toolbar.show-all") }}
</a>
</li>
<li>
<a
@click="
streamConfig.stdout = true;
streamConfig.stderr = false;
"
>
<mdi:check class="w-4" v-if="streamConfig.stdout == true && streamConfig.stderr == false" />
<div v-else class="w-4"></div>
{{ $t("toolbar.show", { std: "STDOUT" }) }}
</a>
</li>
<li>
<a
@click="
streamConfig.stdout = false;
streamConfig.stderr = true;
"
>
<mdi:check class="w-4" v-if="streamConfig.stdout == false && streamConfig.stderr == true" />
<div v-else class="w-4"></div>
{{ $t("toolbar.show", { std: "STDERR" }) }}
</a>
</li>
</ul>
</details>
</li>
<li>
<details class="group/details">
<summary>
<mdi:gauge />
Levels
<Toggle
class="toggle-xs invisible group-open/details:visible"
v-model="toggleAllLevels"
title="Toggle all levels"
/>
</summary>
<ul class="menu">
<li v-for="level in allLevels">
<a class="capitalize" @click="levels.has(level) ? levels.delete(level) : levels.add(level)">
<mdi:check class="w-4" v-if="levels.has(level)" />
<div v-else class="w-4"></div>
<div class="flex">
<div class="badge" :data-level="level">{{ level }}</div>
</div>
</a>
</li>
</ul>
</details>
</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 { Container } from "@/models/Container";
import { allLevels } from "@/composable/logContext";
import LogAnalytics from "../LogViewer/LogAnalytics.vue";
const { showSearch } = useSearchFilter();
const { enableActions } = config;
const { streamConfig, hasComplexLogs, levels } = useLoggingContext();
const showDrawer = useDrawer();
const { container } = defineProps<{ container: Container }>();
const clear = defineEmit();
const { actionStates, start, stop, restart } = useContainerActions(toRef(() => container));
onKeyStroke("f", (e) => {
if (hasComplexLogs.value) {
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
showDrawer(LogAnalytics, { container }, "lg");
e.preventDefault();
}
}
});
const downloadParams = computed(() =>
Object.entries(toValue(streamConfig))
.filter(([, value]) => value)
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), {}),
);
const downloadUrl = computed(() =>
withBase(
`/api/hosts/${container.host}/containers/${container.id}/logs/download?${new URLSearchParams(downloadParams.value).toString()}`,
),
);
const disableRestart = computed(() => actionStates.stop || actionStates.start || actionStates.restart);
const toggleAllLevels = computed({
get: () => levels.value.size === allLevels.length,
set: (value) => {
if (value) {
allLevels.forEach((level) => levels.value.add(level));
} else {
levels.value.clear();
}
},
});
</script>
<style scoped lang="postcss">
li.line {
@apply h-px bg-base-content/20;
}
a {
@apply whitespace-nowrap;
}
.menu li ul {
margin-inline-start: 0;
&:before {
display: none;
}
}
</style>
@@ -0,0 +1,50 @@
<template>
<ScrollableView :scrollable="scrollable" v-if="container">
<template #header v-if="showTitle">
<div class="mx-2 flex items-center gap-2 md:ml-4">
<ContainerTitle :container="container" />
<MultiContainerStat class="ml-auto" :containers="[container]" />
<ContainerActionsToolbar @clear="viewer?.clear()" class="mobile-hidden" :container="container" />
<a class="btn btn-circle btn-xs" @click="close()" v-if="closable">
<mdi:close />
</a>
</div>
</template>
<template #default>
<ViewerWithSource
ref="viewer"
:stream-source="useContainerStream"
:entity="container"
:visible-keys="visibleKeys"
:show-container-name="false"
/>
</template>
</ScrollableView>
</template>
<script lang="ts" setup>
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
import { ComponentExposed } from "vue-component-type-helpers";
const {
id,
showTitle = false,
scrollable = false,
closable = false,
} = defineProps<{
id: string;
showTitle?: boolean;
scrollable?: boolean;
closable?: boolean;
}>();
const close = defineEmit();
const store = useContainerStore();
const container = store.currentContainer(toRef(() => id));
const visibleKeys = persistentVisibleKeysForContainer(container);
const viewer = useTemplateRef<ComponentExposed<typeof ViewerWithSource>>("viewer");
provideLoggingContext(toRef(() => [container.value]));
</script>
@@ -14,25 +14,27 @@
class="mobile-hidden max-w-[1.5em] truncate transition-[max-width] hover:max-w-[400px]"
v-if="container.isSwarm"
>
{{ container.swarmId }}
.{{ container.swarmId }}
</div>
</div>
<container-health :health="container.health" v-if="container.health"></container-health>
<tag class="mobile-hidden hidden font-mono @3xl:block" size="small">
<ContainerHealth :health="container.health" v-if="container.health" />
<Tag class="mobile-hidden hidden font-mono @3xl:block" size="small">
{{ container.image.replace(/@sha.*/, "") }}
</tag>
</Tag>
</div>
</template>
<script lang="ts" setup>
const { container } = useContainerContext();
import { Container } from "@/models/Container";
const { container } = defineProps<{ container: Container }>();
const pinned = computed({
get: () => pinnedContainers.value.has(container.value.name),
get: () => pinnedContainers.value.has(container.name),
set: (value) => {
if (value) {
pinnedContainers.value.add(container.value.name);
pinnedContainers.value.add(container.name);
} else {
pinnedContainers.value.delete(container.value.name);
pinnedContainers.value.delete(container.name);
}
},
});
+7 -6
View File
@@ -31,9 +31,9 @@ function createFuzzySearchModal() {
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", []),
new Container("123", new Date(), "image", "test", "command", "host", {}, "running", []),
new Container("345", new Date(), "image", "foo bar", "command", "host", {}, "running", []),
new Container("567", new Date(), "image", "baz", "command", "host", {}, "running", []),
],
},
},
@@ -56,9 +56,10 @@ describe("<FuzzySearchModal />", () => {
beforeEach(() => {
vi.mocked(useRouter().push).mockReset();
});
test("shows all", async () => {
test("shows none initially", async () => {
const wrapper = createFuzzySearchModal();
expect(wrapper.findAll("li").length).toBe(3);
expect(wrapper.findAll("li").length).toBe(0);
});
test("search for foo", async () => {
@@ -74,6 +75,6 @@ describe("<FuzzySearchModal />", () => {
const wrapper = createFuzzySearchModal();
await wrapper.find("input").setValue("baz");
await wrapper.find("input").trigger("keydown.enter");
expect(useRouter().push).toHaveBeenCalledWith({ name: "container-id", params: { id: "567" } });
expect(useRouter().push).toHaveBeenCalledWith({ name: "/container/[id]", params: { id: "567" } });
});
});
+126 -70
View File
@@ -9,103 +9,152 @@
@keydown.down="selectedIndex = Math.min(selectedIndex + 1, data.length - 1)"
@keydown.up="selectedIndex = Math.max(selectedIndex - 1, 0)"
@keydown.enter.exact="selected(data[selectedIndex].item)"
@keydown.alt.enter="addColumn(data[selectedIndex])"
@keydown.alt.enter="addColumn(data[selectedIndex].item)"
v-model="query"
:placeholder="$t('placeholder.search-containers')"
/>
<mdi:keyboard-esc class="flex" />
<form method="dialog">
<button class="swap swap-rotate hover:swap-active">
<mdi:keyboard-esc class="swap-off" />
<mdi:close class="swap-on" />
</button>
</form>
</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="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>
<distance-time :date="result.item.created" class="text-xs font-light" />
<div
class="dropdown-content !relative mt-2 max-h-[calc(100dvh-20rem)] w-full overflow-y-scroll rounded-md border-y-8 border-transparent bg-base-lighter px-2"
v-if="results.length"
>
<ul tabindex="0" class="menu">
<li v-for="(result, index) in data" ref="listItems">
<a
@click.stop.prevent="addColumn(result.item)"
:title="$t('tooltip.pin-column')"
class="hover:text-secondary"
class="grid auto-cols-max grid-cols-[min-content,auto] gap-2 py-4"
@click.prevent="selected(result.item)"
:class="index === selectedIndex ? 'focus' : ''"
>
<ic:sharp-keyboard-return v-if="index === selectedIndex" />
<cil:columns v-else />
<div :class="{ 'text-primary': result.item.state === 'running' }">
<template v-if="result.item.type === 'container'">
<octicon:container-24 />
</template>
<template v-else-if="result.item.type === 'service'">
<ph:stack-simple />
</template>
<template v-else-if="result.item.type === 'stack'">
<ph:stack />
</template>
</div>
<div class="truncate">
<template v-if="config.hosts.length > 1 && result.item.host">
<span class="font-light">{{ result.item.host }}</span> /
</template>
<span data-name v-html="matchedName(result)"></span>
</div>
<DistanceTime :date="result.item.created" class="text-xs font-light" />
<span
@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-if="result.item.type === 'container'" />
</span>
</a>
</a>
</li>
</ul>
</li>
</ul>
</div>
</div>
</template>
<script lang="ts" setup>
import { ContainerState } from "@/types/Container";
import { useFuse } from "@vueuse/integrations/useFuse";
import { type FuseResultMatch } from "fuse.js";
const { maxResults = 5 } = defineProps<{
maxResults?: number;
}>();
import { type FuseResult } from "fuse.js";
const close = defineEmit();
const query = ref("");
const input = ref<HTMLInputElement>();
const listItems = ref<HTMLInputElement[]>();
const selectedIndex = ref(0);
const router = useRouter();
const store = useContainerStore();
const { containers } = storeToRefs(store);
const containerStore = useContainerStore();
const pinnedStore = usePinnedLogsStore();
const { visibleContainers } = storeToRefs(containerStore);
const swarmStore = useSwarmStore();
const { stacks, services } = storeToRefs(swarmStore);
type Item = {
id: string;
created: Date;
name: string;
state?: ContainerState;
host?: string;
type: "container" | "service" | "stack";
};
const list = computed(() => {
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 items: Item[] = [];
for (const container of visibleContainers.value) {
items.push({
id: container.id,
created: container.created,
name: container.name,
state: container.state,
host: container.hostLabel,
type: "container",
});
}
for (const service of services.value) {
items.push({
id: service.name,
created: service.updatedAt,
name: service.name,
state: "running",
type: "service",
});
}
for (const stack of stacks.value) {
items.push({
id: stack.name,
created: stack.updatedAt,
name: stack.name,
state: "running",
type: "stack",
});
}
return items;
});
const { results } = useFuse(query, list, {
fuseOptions: {
keys: ["name", "host", "labels"],
keys: ["name", "host"],
includeScore: true,
useExtendedSearch: true,
threshold: 0.3,
includeMatches: true,
},
resultLimit: 10,
matchAllWhenSearchEmpty: true,
});
const data = computed(() => {
return [...results.value]
.sort((a, b) => {
if (a.score === b.score) {
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;
}
return [...results.value].sort((a: FuseResult<Item>, b: FuseResult<Item>) => {
if (a.score === b.score) {
if (a.item.state === b.item.state) {
return b.item.created.getTime() - a.item.created.getTime();
} else if (a.item.state === "running" && b.item.state !== "running") {
return -1;
} else {
return a.score - b.score;
return 1;
}
})
.slice(0, maxResults);
} else {
return (a.score ?? 0) - (b.score ?? 0);
}
});
});
watch(query, (data) => {
@@ -114,24 +163,35 @@ watch(query, (data) => {
}
});
onMounted(() => input.value?.focus());
watch(selectedIndex, () => {
listItems.value?.[selectedIndex.value].scrollIntoView({ block: "end" });
});
function selected({ id }: { id: string }) {
router.push({ name: "container-id", params: { id } });
useFocus(input, { initialValue: true });
function selected(item: Item) {
if (item.type === "container") {
router.push({ name: "/container/[id]", params: { id: item.id } });
} else if (item.type === "service") {
router.push({ name: "/service/[name]", params: { name: item.id } });
} else if (item.type === "stack") {
router.push({ name: "/stack/[name]", params: { name: item.id } });
}
close();
}
function addColumn(container: { id: string }) {
store.appendActiveContainer(container);
pinnedStore.pinContainer(container);
close();
}
function matchedName({ item, matches = [] }: { item: { name: string }; matches?: FuseResultMatch[] }) {
function matchedName({ item, matches = [] }: FuseResult<Item>) {
const matched = matches.find((match) => match.key === "name");
if (matched) {
const { indices } = matched;
const result = [];
let lastIndex = 0;
for (const [start, end] of indices) {
if (lastIndex > start) continue;
result.push(item.name.slice(lastIndex, start));
result.push(`<mark>${item.name.slice(start, end + 1)}</mark>`);
lastIndex = end + 1;
@@ -148,8 +208,4 @@ function matchedName({ item, matches = [] }: { item: { name: string }; matches?:
:deep(mark) {
@apply bg-transparent text-inherit underline underline-offset-2;
}
.menu a {
@apply transition-none duration-0;
}
</style>
@@ -0,0 +1,47 @@
<template>
<ScrollableView :scrollable="scrollable" v-if="group.containers.length && ready">
<template #header>
<div class="mx-2 flex items-center gap-2 md:ml-4">
<div class="flex flex-1 gap-1.5 truncate @container md:gap-2">
<div class="inline-flex font-mono text-sm">
<div class="font-semibold">{{ group.containers.length }} containers</div>
</div>
</div>
<MultiContainerStat class="ml-auto" :containers="group.containers" />
<MultiContainerActionToolbar class="mobile-hidden" @clear="viewer?.clear()" />
</div>
</template>
<template #default>
<ViewerWithSource
ref="viewer"
:stream-source="useGroupedStream"
:entity="group"
:visible-keys="new Map<string[], boolean>()"
:show-container-name="true"
/>
</template>
</ScrollableView>
</template>
<script lang="ts" setup>
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
import { GroupedContainers } from "@/models/Container";
import { ComponentExposed } from "vue-component-type-helpers";
const { name, scrollable = false } = defineProps<{
name: string;
scrollable?: boolean;
}>();
const containerStore = useContainerStore();
const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
const { ready } = storeToRefs(containerStore);
const swarmStore = useSwarmStore();
const { customGroups } = storeToRefs(swarmStore);
const group = computed(() => customGroups.value.find((g) => g.name === name) ?? new GroupedContainers("", []));
provideLoggingContext(toRef(() => group.value.containers));
</script>
+118
View File
@@ -0,0 +1,118 @@
<template>
<ul class="grid gap-4 md:grid-cols-[repeat(auto-fill,minmax(480px,1fr))]">
<li v-for="host in hosts" class="card bg-base-lighter">
<div class="card-body grid auto-cols-auto grid-flow-col justify-between gap-4">
<div class="flex flex-col gap-2 overflow-hidden">
<div class="flex items-center gap-1 truncate text-xl font-semibold">
<HostIcon :type="host.type" class="flex-none" />
<div class="truncate">
{{ host.name }}
</div>
<span class="badge badge-error badge-xs gap-2 p-2" v-if="!host.available">
<carbon:warning />
offline
</span>
<span
class="badge badge-success badge-xs gap-2 p-2"
:class="{ 'badge-warning': config.version != host.agentVersion }"
v-else-if="host.type == 'agent'"
title="Dozzle Agent"
>
{{ host.agentVersion }}
</span>
</div>
<ul class="flex flex-row gap-x-2 text-sm md:gap-3">
<li class="flex items-center gap-1"><ph:cpu /> {{ host.nCPU }} <span class="mobile-hidden">CPUs</span></li>
<li class="flex items-center gap-1">
<ph:memory /> {{ formatBytes(host.memTotal) }}
<span class="mobile-hidden">total</span>
</li>
</ul>
<ul class="flex flex-row flex-wrap gap-x-2 text-sm md:gap-3">
<li class="flex items-center gap-1">
<octicon:container-24 class="inline-block" /> {{ $t("label.container", hostContainers[host.id]?.length) }}
</li>
<li class="flex items-center gap-1"><mdi:docker class="inline-block" /> {{ host.dockerVersion }}</li>
</ul>
</div>
<div class="flex flex-row gap-4 md:gap-8" v-if="weightedStats[host.id]">
<div
class="radial-progress text-sm text-primary [--size:4rem] [--thickness:0.25em] md:text-[1rem] md:[--size:5rem]"
:style="`--value: ${Math.floor((weightedStats[host.id].weighted.totalCPU / (host.nCPU * 100)) * 100)}; `"
role="progressbar"
>
{{ weightedStats[host.id].weighted.totalCPU.toFixed(0) }}%
</div>
<div
class="radial-progress text-sm text-primary [--size:4rem] [--thickness:0.25em] md:text-[1rem] md:[--size:5rem]"
:style="`--value: ${(weightedStats[host.id].weighted.totalMem / host.memTotal) * 100};`"
role="progressbar"
>
{{ formatBytes(weightedStats[host.id].weighted.totalMem, 1) }}
</div>
</div>
</div>
</li>
</ul>
</template>
<script setup lang="ts">
import { Container } from "@/models/Container";
const containerStore = useContainerStore();
const { containers } = storeToRefs(containerStore) as unknown as {
containers: Ref<Container[]>;
};
const runningContainers = computed(() => containers.value.filter((container) => container.state === "running"));
const { hosts } = useHosts();
const hostContainers = computed(() => {
const results: Record<string, Container[]> = {};
for (const container of runningContainers.value) {
if (!results[container.host]) {
results[container.host] = [];
}
results[container.host].push(container);
}
return results;
});
type TotalStat = {
totalCPU: number;
totalMem: number;
};
const weightedStats: Record<string, { mostRecent: TotalStat; weighted: TotalStat }> = {};
const initWeightedStats = () => {
for (const [host, containers] of Object.entries(hostContainers.value)) {
const mostRecent = ref<TotalStat>({ totalCPU: 0, totalMem: 0 });
for (const container of containers) {
mostRecent.value.totalCPU += container.stat.cpu;
mostRecent.value.totalMem += container.stat.memoryUsage;
}
weightedStats[host] = reactive({ mostRecent, weighted: useExponentialMovingAverage(mostRecent) });
}
};
watchOnce(hostContainers, initWeightedStats);
initWeightedStats();
useIntervalFn(
() => {
for (const [host, containers] of Object.entries(hostContainers.value)) {
const stat = { totalCPU: 0, totalMem: 0 };
for (const container of containers) {
stat.totalCPU += container.stat.cpu;
stat.totalMem += container.stat.memoryUsage;
}
weightedStats[host].mostRecent = stat;
}
},
1000,
{ immediate: true },
);
</script>
<style scoped></style>
+191
View File
@@ -0,0 +1,191 @@
<template>
<div class="breadcrumbs">
<ul>
<li>
<a @click.prevent="setHost(null)" class="link-primary">{{ $t("label.hosts") }}</a>
</li>
<li v-if="sessionHost && hosts[sessionHost]" class="cursor-default">
{{ hosts[sessionHost].name }}
</li>
</ul>
</div>
<SlideTransition :slide-right="!!sessionHost">
<template #left>
<ul class="menu p-0">
<li v-for="host in hosts" :key="host.id">
<a @click.prevent="setHost(host.id)" :class="{ 'pointer-events-none text-base-content/50': !host.available }">
<HostIcon :type="host.type" />
{{ host.name }}
<span class="badge badge-error badge-xs p-1.5" v-if="!host.available">offline</span>
</a>
</li>
</ul>
</template>
<template #right>
<ul class="containers menu p-0 [&_li.menu-title]:px-0">
<li v-for="{ label, containers, icon } in menuItems" :key="label">
<details :open="!collapsedGroups.has(label)" @toggle="updateCollapsedGroups($event, label)">
<summary class="font-light text-base-content/80">
<component :is="icon" />
{{ label.startsWith("label.") ? $t(label) : label }}
<router-link
:to="{
name: '/merged/[ids]',
params: { ids: containers.map(({ id }) => id).join(',') },
}"
class="btn btn-square btn-outline btn-primary btn-xs"
active-class="btn-active"
:title="$t('tooltip.merge-containers')"
>
<ph:arrows-merge />
</router-link>
</summary>
<ul>
<li v-for="item in containers" :class="item.state" :key="item.id">
<Popup>
<router-link
:to="{ name: '/container/[id]', params: { id: item.id } }"
active-class="active-primary"
@click.alt.stop.prevent="pinnedStore.pinContainer(item)"
:title="item.name"
class="group auto-cols-[auto_max-content_max-content]"
>
<div class="truncate">
{{ item.name }}<span class="font-light opacity-70" v-if="item.isSwarm">.{{ item.swarmId }}</span>
</div>
<ContainerHealth :health="item.health" />
<span
class="hidden hover:text-secondary group-hover:inline-block"
@click.stop.prevent="pinnedStore.pinContainer(item)"
v-show="!pinnedStore.isPinned(item)"
:title="$t('tooltip.pin-column')"
>
<cil:columns />
</span>
</router-link>
<template #content>
<ContainerPopup :container="item as Container" />
</template>
</Popup>
</li>
</ul>
</details>
</li>
</ul>
</template>
</SlideTransition>
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
import { sessionHost } from "@/composable/storage";
// @ts-ignore
import Pin from "~icons/ph/map-pin-simple";
// @ts-ignore
import Stack from "~icons/ph/stack";
// @ts-ignore
import Containers from "~icons/octicon/container-24";
const containerStore = useContainerStore();
const { visibleContainers } = storeToRefs(containerStore);
const pinnedStore = usePinnedLogsStore();
const { hosts } = useHosts();
const setHost = (host: string | null) => (sessionHost.value = host);
const collapsedGroups = useProfileStorage("collapsedGroups", new Set<string>());
const updateCollapsedGroups = (event: Event, label: string) => {
const details = event.target as HTMLDetailsElement;
if (details.open) {
collapsedGroups.value.delete(label);
} else {
collapsedGroups.value.add(label);
}
};
const debouncedPinnedContainers = debouncedRef(pinnedContainers, 200);
const sortedContainers = computed(() =>
visibleContainers.value.filter((c) => c.host === sessionHost.value).sort(sorter),
);
const sorter = (a: Container, b: Container) => {
if (a.state === "running" && b.state !== "running") {
return -1;
} else if (a.state !== "running" && b.state === "running") {
return 1;
} else {
return a.name.localeCompare(b.name);
}
};
const menuItems = computed(() => {
const namespaced: Record<string, Container[]> = {};
const pinned = [];
const singular = [];
for (const item of sortedContainers.value) {
const namespace = item.namespace;
if (debouncedPinnedContainers.value.has(item.name)) {
pinned.push(item);
} else if (namespace) {
namespaced[namespace] ||= [];
namespaced[namespace].push(item);
} else {
singular.push(item);
}
}
const items = [];
if (pinned.length) {
items.push({ label: "label.pinned", containers: pinned, icon: Pin });
}
for (const [label, containers] of Object.entries(namespaced).sort(([a], [b]) => a.localeCompare(b))) {
if (containers.length > 1) {
items.push({ label, containers, icon: Stack });
} else {
singular.push(containers[0]);
}
}
singular.sort(sorter);
if (singular.length) {
items.push({
label: showAllContainers.value ? "label.all-containers" : "label.running-containers",
containers: singular,
icon: Containers,
});
}
return items;
});
const route = useRoute("/container/[id]");
watchEffect(() => {
if (route.name === "/container/[id]") {
const container = containerStore.findContainerById(route.params.id);
if (container) {
setHost(container.host);
}
}
});
</script>
<style scoped lang="postcss">
.menu {
@apply text-[0.95rem];
}
li.exited {
@apply opacity-50;
}
li.deleted {
@apply hidden;
}
</style>
+5 -4
View File
@@ -1,6 +1,7 @@
<template>
<div class="flex items-center justify-end gap-4">
<dropdown class="dropdown-end" @closed="latestTag = latest?.tag ?? config.version">
<slot name="more-items"></slot>
<Dropdown class="dropdown-end" @closed="latestTag = latest?.tag ?? config.version">
<template #trigger>
<mdi:announcement class="size-6 -rotate-12" />
<span
@@ -10,13 +11,13 @@
</template>
<template #content>
<div class="w-72">
<releases />
<Releases />
</div>
</template>
</dropdown>
</Dropdown>
<router-link
:to="{ name: 'settings' }"
:to="{ name: '/settings' }"
:aria-label="$t('title.settings')"
data-testid="settings"
class="btn btn-circle btn-sm"
+15 -24
View File
@@ -1,54 +1,45 @@
<template>
<div class="group/item relative flex w-full gap-x-2" @click="expandToggle()">
<label class="swap swap-rotate invisible absolute -left-4 top-0.5 size-4 group-hover/item:visible">
<input type="checkbox" v-model="expanded" @click="expandToggle()" />
<material-symbols:expand-all-rounded class="swap-off text-secondary" />
<material-symbols:collapse-all-rounded class="swap-on text-secondary" />
</label>
<div class="group/item clickable relative flex w-full gap-x-2" @click="showDrawer(LogDetails, { entry: logEntry })">
<div v-if="showContainerName">
<ContainerName :id="logEntry.containerID" />
</div>
<div v-if="showStd">
<log-std :std="logEntry.std"></log-std>
<LogStd :std="logEntry.std" />
</div>
<div v-if="showTimestamp">
<log-date :date="logEntry.date"></log-date>
<LogDate :date="logEntry.date" class="select-none" />
</div>
<div class="flex">
<log-level :level="logEntry.level"></log-level>
<LogLevel :level="logEntry.level" />
</div>
<div>
<ul class="fields cursor-pointer space-x-4" :class="{ expanded }">
<ul class="fields space-x-4">
<li v-for="(value, name) in validValues" :key="name">
<span class="text-light">{{ name }}=</span><span class="font-bold" v-if="value === null">&lt;null&gt;</span>
<template v-else-if="Array.isArray(value)">
<span class="font-bold" v-html="markSearch(JSON.stringify(value))"> </span>
<span class="font-bold" v-html="JSON.stringify(value)"> </span>
</template>
<span class="font-bold" v-html="markSearch(value)" v-else></span>
<span class="font-bold" v-html="value" v-else></span>
</li>
<li class="text-light" v-if="Object.keys(validValues).length === 0">all values are hidden</li>
</ul>
<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>
import { type ComplexLogEntry } from "@/models/LogEntry";
import LogDetails from "./LogDetails.vue";
const { markSearch } = useSearchFilter();
const { logEntry } = defineProps<{
const { logEntry, showContainerName = false } = defineProps<{
logEntry: ComplexLogEntry;
visibleKeys: string[][];
showContainerName?: boolean;
}>();
const [expanded, expandToggle] = useToggle();
const validValues = computed(() => {
return Object.fromEntries(Object.entries(logEntry.message).filter(([_, value]) => value !== undefined));
});
const showDrawer = useDrawer();
</script>
<style lang="postcss" scoped>
@@ -0,0 +1,88 @@
<template>
<div class="flex w-full flex-col">
<div class="relative flex items-start gap-x-2">
<ContainerName class="flex-none" :id="logEntry.containerID" v-if="showContainerName" />
<LogDate :date="logEntry.date" v-if="showTimestamp" />
<LogLevel class="flex" />
<div class="whitespace-pre-wrap" :data-event="logEntry.event" v-html="logEntry.message"></div>
</div>
<div class="alert alert-info mt-8 w-auto flex-none font-sans text-[1rem] md:mx-auto md:w-1/2" v-if="followEligible">
<carbon:information class="size-6 shrink-0 stroke-current" />
<div>
<h3 class="text-lg font-bold">{{ $t("alert.similar-container-found.title") }}</h3>
{{ $t("alert.similar-container-found.message", { containerId: nextContainer.id }) }}
</div>
<div>
<TimedButton v-if="automaticRedirect" class="btn-primary btn-sm" @finished="redirectNow()">{{
$t("button.cancel")
}}</TimedButton>
<router-link
:to="{ name: '/container/[id]', params: { id: nextContainer.id } }"
class="btn btn-primary btn-sm"
v-else
>
{{ $t("button.redirect") }}
</router-link>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ContainerEventLogEntry } from "@/models/LogEntry";
const router = useRouter();
const { showToast } = useToast();
const { t } = useI18n();
const { logEntry } = defineProps<{
logEntry: ContainerEventLogEntry;
showContainerName?: boolean;
}>();
const { containers } = useLoggingContext();
const store = useContainerStore();
const { containers: allContainers } = storeToRefs(store);
const nextContainer = computed(
() =>
[
...allContainers.value.filter(
(c) =>
c.host === containers.value[0].host &&
c.created > logEntry.date &&
c.name === containers.value[0].name &&
c.state === "running",
),
].sort((a, b) => +a.created - +b.created)[0],
);
const followEligible = computed(
() =>
router.currentRoute.value.name === "/container/[id]" &&
logEntry.event === "container-stopped" &&
containers.value.length === 1 &&
nextContainer.value !== undefined,
);
function redirectNow() {
showToast(
{
title: t("alert.redirected.title"),
message: t("alert.redirected.message", { containerId: nextContainer.value?.id }),
type: "info",
},
{ expire: 5000 },
);
router.push({ name: "/container/[id]", params: { id: nextContainer.value?.id } });
}
</script>
<style lang="postcss" scoped>
[data-event="container-stopped"] {
@apply text-red;
}
[data-event="container-started"] {
@apply text-green;
}
</style>
@@ -1,39 +0,0 @@
<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>
@@ -0,0 +1,41 @@
<template>
<div class="grid w-40 overflow-hidden rounded text-center text-sm text-white">
<div class="random-color col-start-1 row-start-1 brightness-75"></div>
<div class="col-start-1 row-start-1 truncate px-2 brightness-100 [direction:rtl]">{{ containerNames[id] }}</div>
</div>
</template>
<script lang="ts">
const colors = [
"#4B0082",
"#FF00FF",
"#FF7F00",
"#FFFF00",
"#00FF00",
"#00FFFF",
"#FF0000",
"#0000FF",
"#FF007F",
"#32CD32",
"#40E0D0",
"#E6E6FA",
"#800080",
"#FFD700",
"#FF4040",
] as const;
</script>
<script lang="ts" setup>
const containerStore = useContainerStore();
const { containerNames } = storeToRefs(containerStore);
const { id } = defineProps<{
id: string;
}>();
const color = computed(() => colors[Math.abs(hashCode(id)) % colors.length]);
</script>
<style lang="postcss" scoped>
.random-color {
background-color: v-bind(color);
}
</style>
@@ -1,36 +0,0 @@
<template>
<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>
const { container } = useContainerContext();
const cpuData = computedWithControl(
() => container.value.stat,
() => {
const history = container.value.statsHistory;
const points: Point<unknown>[] = history.map((stat, i) => ({
x: i,
y: Math.max(0, stat.cpu),
value: Math.max(0, stat.cpu).toFixed(2) + "%",
}));
return points;
},
);
const memoryData = computedWithControl(
() => container.value.stat,
() => {
const history = container.value.statsHistory;
const points: Point<string>[] = history.map((stat, i) => ({
x: i,
y: stat.memory,
value: formatBytes(stat.memoryUsage),
}));
return points;
},
);
</script>
@@ -1,73 +0,0 @@
<template>
<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();
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",
),
].sort((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="postcss" scoped>
[data-event="container-stopped"] {
@apply text-red;
}
[data-event="container-started"] {
@apply text-green;
}
</style>
@@ -1,6 +1,5 @@
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
@@ -9,8 +8,10 @@ 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";
import { default as Component } from "./EventSource.vue";
import LogViewer from "@/components/LogViewer/LogViewer.vue";
import { Container } from "@/models/Container";
import { Level } from "@/models/LogEntry";
vi.mock("@/stores/config", () => ({
__esModule: true,
@@ -21,7 +22,7 @@ vi.mock("@/stores/config", () => ({
/**
* @vitest-environment jsdom
*/
describe("<LogEventSource />", () => {
describe("<ContainerEventSource />", () => {
const search = useSearchFilter();
beforeEach(() => {
@@ -50,7 +51,7 @@ describe("<LogEventSource />", () => {
},
) {
settings.value.hourStyle = hourStyle;
search.searchFilter.value = searchFilter;
search.searchQueryFilter.value = searchFilter;
if (searchFilter) {
search.showSearch.value = true;
}
@@ -67,32 +68,40 @@ describe("<LogEventSource />", () => {
],
});
return mount(LogEventSource, {
return mount(Component, {
global: {
plugins: [router, createTestingPinia({ createSpy: vi.fn }), createI18n({})],
components: {
ContainerLogViewer,
LogViewer,
},
provide: {
[containerContext as symbol]: {
container: computed(() => ({ id: "abc", image: "test:v123", host: "localhost" })),
streamConfig: reactive({ stdout: true, stderr: true }),
[scrollContextKey as symbol]: {
paused: computed(() => false),
loading: computed(() => false),
},
[loggingContextKey as symbol]: {
containers: computed(() => [{ id: "abc", image: "test:v123", host: "localhost" }]),
streamConfig: reactive({ stdout: true, stderr: true }),
hasComplexLogs: ref(false),
levels: new Set<Level>(["info"]),
},
scrollingPaused: computed(() => false),
},
},
slots: {
default: `
<template #scoped="params"><container-log-viewer :messages="params.messages" /></template>
<template #scoped="params"><LogViewer :messages="params.messages" :show-container-name="false" :visible-keys="[]" /></template>
`,
},
props: {},
props: {
streamSource: useContainerStream,
entity: new Container("abc", new Date(), "image", "name", "command", "localhost", {}, "created", []),
},
});
}
const sourceUrl = "/api/hosts/localhost/containers/abc/logs/stream?stdout=1&stderr=1";
const sourceUrl = "/api/hosts/localhost/containers/abc/logs/stream?stdout=1&stderr=1&levels=info";
test("renders correctly", async () => {
test("renders loading correctly", async () => {
const wrapper = createLogEventSource();
expect(wrapper.html()).toMatchSnapshot();
});
@@ -140,24 +149,11 @@ describe("<LogEventSource />", () => {
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
});
test("should render messages with html entities", async () => {
const wrapper = createLogEventSource();
sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
});
vi.runAllTimers();
await nextTick();
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
});
test("should render dates with 12 hour style", async () => {
const wrapper = createLogEventSource({ hourStyle: "12" });
sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
data: `{"ts":1560336942459, "m":"foo bar", "id":1}`,
});
vi.runAllTimers();
@@ -169,25 +165,9 @@ describe("<LogEventSource />", () => {
test("should render dates with 24 hour style", async () => {
const wrapper = createLogEventSource({ hourStyle: "24" });
sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
});
vi.runAllTimers();
await nextTick();
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
});
test("should render messages with filter", async () => {
const wrapper = createLogEventSource({ searchFilter: "test" });
sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"foo bar", "id":1}`,
});
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"test bar", "id":2}`,
});
vi.runAllTimers();
await nextTick();
@@ -0,0 +1,32 @@
<template>
<InfiniteLoader :onLoadMore="fetchMore" :enabled="!loadingMore && messages.length > 10" />
<slot :messages="messages"></slot>
</template>
<script lang="ts" setup generic="T">
import { LogStreamSource } from "@/composable/eventStreams";
const { entity, streamSource } = $defineProps<{
streamSource: (t: Ref<T>) => LogStreamSource;
entity: T;
}>();
const { messages, loadOlderLogs, isLoadingMore } = streamSource($$(entity));
const { loadingMore } = useLoggingContext();
const enabled = ref(true);
defineExpose({
clear: () => (messages.value = []),
});
const fetchMore = async () => {
if (!isLoadingMore.value) {
loadingMore.value = true;
enabled.value = false;
await loadOlderLogs();
loadingMore.value = false;
enabled.value = true;
}
};
</script>
-79
View File
@@ -1,79 +0,0 @@
<template>
<ul v-if="expanded" ref="root" class="ml-8">
<li v-for="(value, name) in fields" :key="name">
<template v-if="isObject(value)">
<span class="text-light">{{ name }}=</span>
<field-list
:fields="value"
:parent-key="parentKey.concat(name)"
:visible-keys="visibleKeys"
expanded
></field-list>
</template>
<template v-else-if="Array.isArray(value)">
<a @click.stop="toggleField(name)" class="link-primary mr-2 cursor-pointer">
{{ hasField(name) ? "remove" : "add" }}
</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.stop="toggleField(name)" class="link-primary mr-2 cursor-pointer">
{{ hasField(name) ? "remove" : "add" }}
</a>
<span class="text-light">{{ name }}=</span><span class="font-bold" v-html="value"></span>
</template>
</li>
</ul>
</template>
<script lang="ts" setup>
import { arrayEquals, isObject } from "@/utils";
const {
fields,
expanded = false,
parentKey = [],
visibleKeys = [],
} = defineProps<{
fields: Record<string, any>;
expanded?: boolean;
parentKey?: string[];
visibleKeys?: string[][];
}>();
const root = ref<HTMLElement>();
async function toggleField(field: string) {
const index = fieldIndex(field);
if (index > -1) {
visibleKeys.splice(index, 1);
} else {
visibleKeys.push(parentKey.concat(field));
}
await nextTick();
root.value?.scrollIntoView({
block: "center",
});
}
function hasField(field: string) {
return fieldIndex(field) > -1;
}
function fieldIndex(field: string) {
const path = parentKey.concat(field);
return visibleKeys.findIndex((keys) => arrayEquals(toRaw(keys), toRaw(path)));
}
</script>
<style scoped lang="postcss">
.text-light {
@apply text-base-content/70;
}
</style>
@@ -0,0 +1,97 @@
<template>
<aside>
<header class="flex items-center gap-4">
<h1 class="mobile-hidden text-2xl">{{ container.name }}</h1>
<h2 class="text-sm"><DistanceTime :date="container.created" /></h2>
</header>
<div class="mt-8 flex flex-col gap-2">
<section>
<label class="form-control">
<textarea
v-model="query"
class="textarea textarea-primary w-full font-mono text-lg"
:class="{ 'textarea-error': error }"
></textarea>
<div class="label">
<span class="label-text-alt text-error" v-if="error">{{ error }}</span>
<span class="label-text-alt" v-else>
Total {{ results.numRows }} records
<template v-if="results.numRows > pageLimit">. Showing first {{ page.numRows }}.</template></span
>
</div>
</label>
</section>
<SQLTable :table="page" :loading="evaluating || !isReady" />
</div>
</aside>
</template>
<script setup lang="ts">
import { Container } from "@/models/Container";
import { type Table } from "@apache-arrow/esnext-esm";
const { container } = defineProps<{ container: Container }>();
const query = ref("SELECT * FROM logs LIMIT 100");
const error = ref<string | null>(null);
const debouncedQuery = debouncedRef(query, 500);
const evaluating = ref(false);
const pageLimit = 1000;
const url = withBase(
`/api/hosts/${container.host}/containers/${container.id}/logs?stdout=1&stderr=1&everything&jsonOnly`,
);
const [{ useDuckDB }, response] = await Promise.all([import(`@/composable/duckdb`), fetch(url)]);
if (!response.ok) {
console.log("error fetching logs from", url);
throw new Error(`Failed to fetch logs: ${response.statusText}`);
}
const { db, conn } = await useDuckDB();
const empty = await conn.query<Record<string, any>>(`SELECT 1 LIMIT 0`);
const { isReady } = useAsyncState(
async () => {
await db.registerFileBuffer("logs.json", new Uint8Array(await response.arrayBuffer()));
await conn.query(
`CREATE TABLE logs AS SELECT unnest(m) FROM read_json('logs.json', ignore_errors = true, format = 'newline_delimited')`,
);
},
undefined,
{
onError: (e) => {
console.error(e);
if (e instanceof Error) {
error.value = e.message;
}
},
},
);
const results = computedAsync(
async () => {
if (isReady.value) {
return await conn.query<Record<string, any>>(debouncedQuery.value);
} else {
return empty;
}
},
empty,
{
onError: (e) => {
console.error(e);
if (e instanceof Error) {
error.value = e.message;
}
},
evaluating,
},
);
whenever(evaluating, () => (error.value = null));
const page = computed(() =>
results.value.numRows > pageLimit ? results.value.slice(0, pageLimit) : results.value,
) as unknown as ComputedRef<Table<Record<string, any>>>;
</script>
<style lang="postcss" scoped></style>
@@ -1,53 +0,0 @@
<template>
<scrollable-view :scrollable="scrollable" v-if="container">
<template #header v-if="showTitle">
<div class="mx-2 flex items-center gap-2 md:ml-4">
<container-title @close="$emit('close')" />
<container-stat class="ml-auto" />
<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 }">
<log-viewer-with-source ref="viewer" @loading-more="setLoading($event)" />
</template>
</scrollable-view>
</template>
<script lang="ts" setup>
import LogViewerWithSource from "./LogViewerWithSource.vue";
const {
id,
showTitle = false,
scrollable = false,
closable = false,
} = defineProps<{
id: string;
showTitle?: boolean;
scrollable?: boolean;
closable?: boolean;
}>();
const close = defineEmit();
const store = useContainerStore();
const container = store.currentContainer($$(id));
provideContainerContext(container);
const viewer = ref<InstanceType<typeof LogViewerWithSource>>();
function onClearClicked() {
viewer.value?.clear();
}
onKeyStroke("k", (e) => {
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
onClearClicked();
e.preventDefault();
}
});
</script>
+3 -3
View File
@@ -1,7 +1,7 @@
<template>
<tag size="small">
<date-time :date="date" class="whitespace-nowrap text-blue"></date-time>
</tag>
<Tag size="small">
<DateTime :date="date" class="whitespace-nowrap text-blue" />
</Tag>
</template>
<script lang="ts" setup>
defineProps<{
+197
View File
@@ -0,0 +1,197 @@
<template>
<header class="flex items-center gap-4">
<Tag :data-level="entry.level" class="uppercase text-white" v-if="entry.level">{{ entry.level }}</Tag>
<h1 class="mobile-hidden text-lg">
<DateTime :date="entry.date" />
</h1>
<h2 class="text-sm"><DistanceTime :date="entry.date" /> on {{ entry.std }}</h2>
</header>
<div class="mt-8 flex flex-col gap-10">
<section class="grid grid-cols-3 gap-2">
<div>
<div class="font-thin">Container Name</div>
<div class="truncate text-lg font-bold">{{ container.name }}</div>
</div>
<div>
<div class="font-thin">Host</div>
<div class="truncate text-lg font-bold">
{{ hosts[container.host].name }}
</div>
</div>
<div>
<div class="font-thin">Image</div>
<div class="truncate text-lg font-bold">{{ container.image }}</div>
</div>
</section>
<section class="flex flex-col gap-2">
<div class="flex gap-2 font-thin">
Raw JSON
<UseClipboard v-slot="{ copy, copied }" :source="JSON.stringify(entry.unfilteredMessage)">
<button class="swap swap-flip outline-none" @click="copy()" :class="{ 'hover:swap-active': copied }">
<mdi:check class="swap-on" />
<mdi:content-copy class="swap-off" />
</button>
</UseClipboard>
</div>
<div class="max-h-48 overflow-scroll rounded border border-base-lighter bg-base-darker p-2">
<pre v-html="syntaxHighlight(entry.unfilteredMessage)"></pre>
</div>
</section>
<table class="table table-pin-rows table-fixed" v-if="entry instanceof ComplexLogEntry">
<caption class="caption-bottom">
Fields are sortable by dragging and dropping.
</caption>
<thead class="text-lg">
<tr>
<th class="w-60">Field</th>
<th class="mobile-hidden">Value</th>
<th class="w-20">
<input type="checkbox" class="toggle toggle-primary" v-model="toggleAllFields" title="Toggle all" />
</th>
</tr>
</thead>
<tbody ref="list">
<tr v-for="{ key, value, enabled } in fields" :key="key.join('.')" class="hover">
<td class="cursor-move break-all font-mono">
{{ key.join(".") }}
</td>
<td class="mobile-hidden truncate">
<code v-html="JSON.stringify(value)"></code>
</td>
<td>
<input type="checkbox" class="toggle toggle-primary" :checked="enabled" @change="toggleField(key)" />
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup lang="ts">
import { ComplexLogEntry } from "@/models/LogEntry";
import { useSortable } from "@vueuse/integrations/useSortable";
import { UseClipboard } from "@vueuse/components";
const { entry } = defineProps<{ entry: ComplexLogEntry }>();
const { currentContainer } = useContainerStore();
const list = ref<HTMLElement>();
const container = currentContainer(toRef(() => entry.containerID));
const visibleKeys = persistentVisibleKeysForContainer(container);
const { hosts } = useHosts();
function toggleField(key: string[]) {
if (visibleKeys.value.size === 0) {
visibleKeys.value = new Map<string[], boolean>(fields.value.map(({ key }) => [key, true]));
}
const enabled = visibleKeys.value.get(key) ?? true;
visibleKeys.value.set(key, !enabled);
}
const fields = computed({
get() {
const fieldsWithValue: { key: string[]; value: any; enabled: boolean }[] = [];
const allFields = flattenJSONToMap(entry.unfilteredMessage);
if (visibleKeys.value.size === 0) {
for (const [key, value] of allFields) {
fieldsWithValue.push({ key, value, enabled: true });
}
} else {
for (const [key, enabled] of visibleKeys.value) {
const value = getDeep(entry.unfilteredMessage, key);
fieldsWithValue.push({ key, value, enabled });
}
for (const [key, value] of allFields) {
if ([...visibleKeys.value.keys()].findIndex((k) => arrayEquals(k, key)) === -1) {
fieldsWithValue.push({ key, value, enabled: true });
}
}
}
return fieldsWithValue;
},
set(value) {
const map = new Map<string[], boolean>();
for (const { key, enabled } of value) {
map.set(key, enabled);
}
visibleKeys.value = map;
},
});
const toggleAllFields = computed({
get: () => fields.value.every(({ enabled }) => enabled),
set(value) {
if (visibleKeys.value.size === 0) {
visibleKeys.value = new Map<string[], boolean>(fields.value.map(({ key }) => [key, true]));
}
for (const key of visibleKeys.value.keys()) {
visibleKeys.value.set(key, value);
}
for (const field of fields.value) {
visibleKeys.value.set(field.key, value);
}
},
});
function syntaxHighlight(json: any) {
json = JSON.stringify(json, null, 2);
return json.replace(
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|\b\d+\b)/g,
function (match: string) {
var cls = "json-number";
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = "json-key";
} else {
cls = "json-string";
}
} else if (/true|false/.test(match)) {
cls = "json-boolean";
} else if (/null/.test(match)) {
cls = "json-null";
}
return `<span class="${cls}">${match}</span>`;
},
);
}
useSortable(list, fields);
</script>
<style lang="postcss" scoped>
.font-mono {
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Consolas,
Liberation Mono,
monaco,
Menlo,
monospace;
}
pre {
:deep(.json-key) {
@apply text-blue;
}
:deep(.json-string) {
@apply text-green;
}
:deep(.json-number) {
@apply text-orange;
}
:deep(.json-boolean) {
@apply text-purple;
}
:deep(.json-null) {
@apply text-red;
}
}
</style>
@@ -1,19 +0,0 @@
<template>
<infinite-loader :onLoadMore="fetchMore" :enabled="messages.length > 100"></infinite-loader>
<slot :messages="messages"></slot>
</template>
<script lang="ts" setup>
const loadingMore = defineEmit<[value: boolean]>();
const { messages, loadOlderLogs } = useLogStream();
const beforeLoading = () => loadingMore(true);
const afterLoading = () => loadingMore(false);
defineExpose({
clear: () => (messages.value = []),
});
const fetchMore = () => loadOlderLogs({ beforeLoading, afterLoading });
</script>
+8 -5
View File
@@ -32,23 +32,26 @@ defineProps<{
margin-top: -0.4em;
align-self: flex-start;
}
</style>
<style lang="postcss">
[data-level="debug"],
[data-level="trace"] {
@apply bg-purple;
@apply !bg-purple;
}
[data-level="info"] {
@apply bg-green;
@apply !bg-green;
}
[data-level="error"],
[data-level="severe"],
[data-level="critical"],
[data-level="fatal"] {
@apply bg-red;
@apply !bg-red;
}
[data-level="warn"],
[data-level="warning"] {
@apply bg-orange;
@apply !bg-orange;
}
</style>
+106
View File
@@ -0,0 +1,106 @@
<template>
<ul
class="events group pt-4"
:class="{ 'disable-wrap': !softWrap, [size]: true, compact }"
v-if="messages.length > 0"
>
<li
v-for="item in messages"
ref="list"
:key="item.id"
:data-key="item.id"
:data-time="item.date.getTime()"
class="group/entry"
>
<component :is="item.getComponent()" :log-entry="item" :show-container-name="showContainerName" />
</li>
</ul>
</template>
<script lang="ts" setup>
import { type JSONObject, LogEntry } from "@/models/LogEntry";
const { loading, progress, currentDate } = useScrollContext();
const { messages } = defineProps<{
messages: LogEntry<string | JSONObject>[];
showContainerName: boolean;
}>();
watchEffect(() => {
loading.value = messages.length === 0;
});
const { containers } = useLoggingContext();
const list = ref<HTMLElement[]>([]);
useIntersectionObserver(
list,
(entries) => {
if (containers.value.length != 1) return;
const container = containers.value[0];
for (const entry of entries) {
if (entry.isIntersecting) {
const time = entry.target.getAttribute("data-time");
if (time) {
const date = new Date(parseInt(time));
const diff = new Date().getTime() - container.created.getTime();
progress.value = (date.getTime() - container.created.getTime()) / diff;
currentDate.value = date;
}
}
}
},
{
rootMargin: "-10% 0px -10% 0px",
threshold: 1,
},
);
</script>
<style scoped lang="postcss">
.events {
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Consolas,
Liberation Mono,
monaco,
Menlo,
monospace;
> li {
@apply flex break-words px-2 py-1 last:snap-end odd:bg-gray-400/[0.07] has-[.clickable]:cursor-pointer has-[.clickable]:hover:bg-primary/10 md:px-4;
&:last-child {
scroll-margin-block-end: 5rem;
}
.jump-context {
@apply mr-2 flex items-center font-sans text-secondary;
}
}
&.small {
@apply text-[0.7em];
}
&.medium {
@apply text-[0.8em];
}
&.large {
@apply text-lg;
}
&.compact {
> li {
@apply py-0;
}
:deep(.tag) {
@apply rounded-none;
}
}
}
</style>
@@ -7,31 +7,18 @@
>
<span
class="rounded bg-slate-800/60 px-1.5 py-1 text-primary hover:bg-slate-700"
@click="copyLogMessageToClipBoard()"
@click.prevent="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<{
const { message } = defineProps<{
message: () => string;
logEntry: LogEntry<string | JSONObject>;
}>();
@@ -40,9 +27,6 @@ const { showToast } = useToast();
const { copy, isSupported, copied } = useClipboard();
const { t } = useI18n();
const { isSearching } = useSearchFilter();
const { handleJumpLineSelected } = useLogSearchContext();
async function copyLogMessageToClipBoard() {
await copy(message());
+2 -2
View File
@@ -1,7 +1,7 @@
<template>
<tag size="small" :std="std">
<Tag size="small" :std="std">
{{ std }}
</tag>
</Tag>
</template>
<script lang="ts" setup>
+34 -58
View File
@@ -1,67 +1,43 @@
<template>
<ul class="events group py-4" :class="{ 'disable-wrap': !softWrap, [size]: true, compact }">
<li
v-for="item in messages"
:key="item.id"
:data-key="item.id"
:class="{ 'border border-secondary': toRaw(item) === toRaw(lastSelectedItem) }"
class="group/entry"
>
<component :is="item.getComponent()" :log-entry="item" :visible-keys="visibleKeys" />
</li>
</ul>
<LogList :messages="visibleMessages" :show-container-name="showContainerName" />
</template>
<script lang="ts" setup>
import { toRaw } from "vue";
import { type JSONObject, LogEntry } from "@/models/LogEntry";
defineProps<{
const props = defineProps<{
messages: LogEntry<string | JSONObject>[];
visibleKeys: string[][];
lastSelectedItem: LogEntry<string | JSONObject> | undefined;
visibleKeys: Map<string[], boolean>;
showContainerName: boolean;
}>();
const { messages, visibleKeys } = toRefs(props);
const { filteredPayload } = useVisibleFilter(visibleKeys);
const { debouncedSearchFilter } = useSearchFilter();
const { streamConfig } = useLoggingContext();
const visibleMessages = filteredPayload(messages);
const router = useRouter();
watchEffect(() => {
const query = {} as Record<string, string>;
if (debouncedSearchFilter.value !== "") {
query.search = debouncedSearchFilter.value;
}
if (!streamConfig.value.stderr) {
query.stderr = streamConfig.value.stderr.toString();
}
if (!streamConfig.value.stdout) {
query.stdout = streamConfig.value.stdout.toString();
}
router.push({
query,
replace: true,
});
});
</script>
<style scoped lang="postcss">
.events {
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Consolas,
Liberation Mono,
monaco,
Menlo,
monospace;
> li {
@apply flex break-words px-2 py-1 last:snap-end odd:bg-gray-400/[0.07] md:px-4;
&:last-child {
scroll-margin-block-end: 5rem;
}
.jump-context {
@apply mr-2 flex items-center font-sans text-secondary;
}
}
&.small {
@apply text-[0.7em];
}
&.medium {
@apply text-[0.8em];
}
&.large {
@apply text-lg;
}
&.compact {
> li {
@apply py-0;
}
}
}
</style>
<style scoped lang="postcss"></style>
@@ -1,18 +0,0 @@
<template>
<log-event-source ref="source" #default="{ messages }" @loading-more="loadingMore($event)">
<container-log-viewer :messages="messages" />
</log-event-source>
</template>
<script lang="ts" setup>
import LogEventSource from "./LogEventSource.vue";
const loadingMore = defineEmit<[value: boolean]>();
const source = $ref<InstanceType<typeof LogEventSource>>();
function clear() {
source?.clear();
}
defineExpose({
clear,
});
</script>
@@ -8,18 +8,13 @@
<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/hosts/${container.host}/containers/${container.id}/logs/download`" download>
<octicon:download-24 /> {{ $t("toolbar.download") }}
<KeyShortcut char="k" :modifiers="['shift', 'meta']" />
</a>
</li>
<li>
<a @click.prevent="showSearch = true">
<mdi:magnify /> {{ $t("toolbar.search") }}
<key-shortcut char="f"></key-shortcut>
<KeyShortcut char="f" />
</a>
</li>
<li class="line"></li>
@@ -65,57 +60,16 @@
{{ $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>
const { showSearch } = useSearchFilter();
const { base, enableActions } = config;
const clear = defineEmit();
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;
});
const { streamConfig } = useLoggingContext();
</script>
<style scoped lang="postcss">
@@ -0,0 +1,73 @@
<template>
<div class="flex gap-4">
<StatMonitor :data="memoryData" label="mem" :stat-value="formatBytes(totalStat.memoryUsage)" />
<StatMonitor :data="cpuData" label="load" :stat-value="Math.max(0, totalStat.cpu).toFixed(2) + '%'" />
</div>
</template>
<script lang="ts" setup>
import { Stat } from "@/models/Container";
import { Container } from "@/models/Container";
const { containers } = defineProps<{
containers: Container[];
}>();
const totalStat = ref<Stat>({ cpu: 0, memory: 0, memoryUsage: 0 });
const { history, reset } = useSimpleRefHistory(totalStat, { capacity: 300 });
watch(
() => containers,
() => {
const initial: Stat[] = [];
for (let i = 1; i <= 300; i++) {
const stat = containers.reduce(
(acc, { statsHistory }) => {
const item = statsHistory.at(-i);
if (!item) {
return acc;
}
return {
cpu: acc.cpu + item.cpu,
memory: acc.memory + item.memory,
memoryUsage: acc.memoryUsage + item.memoryUsage,
};
},
{ cpu: 0, memory: 0, memoryUsage: 0 },
);
initial.push(stat);
}
reset({ initial: initial.reverse() });
},
{ immediate: true },
);
useIntervalFn(() => {
totalStat.value = containers.reduce(
(acc, { stat }) => {
return {
cpu: acc.cpu + stat.cpu,
memory: acc.memory + stat.memory,
memoryUsage: acc.memoryUsage + stat.memoryUsage,
};
},
{ cpu: 0, memory: 0, memoryUsage: 0 },
);
}, 1000);
const cpuData = computed(() =>
history.value.map((stat, i) => ({
x: i,
y: Math.max(0, stat.cpu),
value: Math.max(0, stat.cpu).toFixed(2) + "%",
})),
);
const memoryData = computed(() =>
history.value.map((stat, i) => ({
x: i,
y: stat.memoryUsage,
value: formatBytes(stat.memoryUsage),
})),
);
</script>
+40
View File
@@ -0,0 +1,40 @@
<template>
<table class="table table-zebra table-pin-rows table-md" v-if="!loading">
<thead>
<tr>
<th v-for="column in columns" :key="column">{{ column }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in table" :key="row">
<td v-for="column in columns" :key="column">{{ row[column] }}</td>
</tr>
</tbody>
</table>
<table class="table table-md animate-pulse" v-else>
<thead>
<tr>
<th v-for="_ in 3">
<div class="h-4 w-20 animate-pulse bg-base-content/50 opacity-50"></div>
</th>
</tr>
</thead>
<tbody>
<tr v-for="_ in 9">
<td v-for="_ in 3">
<div class="h-4 w-20 bg-base-content/50 opacity-20"></div>
</td>
</tr>
</tbody>
</table>
</template>
<script lang="ts" setup>
import { type Table } from "@apache-arrow/esnext-esm";
const { loading, table } = defineProps<{
loading: boolean;
table: Table<Record<string, any>>;
}>();
const columns = computed(() => (table.numRows > 0 ? Object.keys(table.get(0) as Record<string, any>) : []));
</script>
+20 -11
View File
@@ -1,13 +1,14 @@
<template>
<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" />
<LogStd :std="logEntry.std" v-if="showStd" />
<ContainerName class="flex-none" :id="logEntry.containerID" v-if="showContainerName" />
<LogDate :date="logEntry.date" v-if="showTimestamp" class="select-none" />
<LogLevel class="flex" :level="logEntry.level" :position="logEntry.position" />
<div
class="whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap"
v-html="colorize(logEntry.message)"
class="log-wrapper whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap"
v-html="linkify(colorize(logEntry.message))"
></div>
<log-message-actions
<LogMessageActions
class="duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100"
:message="() => decodeXML(stripAnsi(logEntry.message))"
:log-entry="logEntry"
@@ -22,14 +23,22 @@ import stripAnsi from "strip-ansi";
const ansiConvertor = new AnsiConvertor({
escapeXML: false,
fg: "var(--base-content-color)",
bg: "var(--base-color)",
fg: "oklch(var(--base-content-color))",
bg: "oklch(var(--base-color))",
});
defineProps<{
const { showContainerName = false } = defineProps<{
logEntry: SimpleLogEntry;
showContainerName?: boolean;
}>();
const { markSearch } = useSearchFilter();
const colorize = (value: string) => markSearch(ansiConvertor.toHtml(value));
const colorize = (value: string) => ansiConvertor.toHtml(value);
const urlPattern = /(https?:\/\/[^\s]+)/g;
const linkify = (text: string) =>
text.replace(urlPattern, (url) => `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`);
</script>
<style scoped lang="postcss">
.log-wrapper :deep(a) {
@apply text-primary underline-offset-4 hover:underline;
}
</style>
@@ -1,10 +1,10 @@
<template>
<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>
<ZigZag class="absolute inset-0 mt-2" />
<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>
+1 -1
View File
@@ -1,7 +1,7 @@
<template>
<div class="relative hover:text-secondary" @mouseenter="mouseOver = true" @mouseleave="mouseOver = false">
<div class="hidden overflow-hidden rounded-sm border border-primary px-px pb-px pt-1 md:flex">
<stat-sparkline :data="data" @selected-point="onSelectedPoint"></stat-sparkline>
<StatSparkline :data="data" @selected-point="onSelectedPoint" />
</div>
<div class="inline-flex gap-1 rounded bg-base p-px text-xs md:absolute md:-left-0.5 md:-top-2">
<div class="font-light uppercase">{{ label }}</div>
@@ -0,0 +1,31 @@
<template>
<EventSource ref="source" #default="{ messages }" :stream-source="streamSource" :entity="entity">
<LogViewer :messages="messages" :visible-keys="visibleKeys" :show-container-name="showContainerName" />
</EventSource>
</template>
<script lang="ts" setup generic="T">
import EventSource from "@/components/LogViewer/EventSource.vue";
import { LogStreamSource } from "@/composable/eventStreams";
import { ComponentExposed } from "vue-component-type-helpers";
const { streamSource, visibleKeys, showContainerName, entity } = defineProps<{
streamSource: (t: Ref<T>) => LogStreamSource;
visibleKeys: Map<string[], boolean>;
showContainerName: boolean;
entity: T;
}>();
const source = useTemplateRef<ComponentExposed<typeof EventSource>>("source");
defineExpose({
clear: () => source.value?.clear(),
});
onKeyStroke("k", (e) => {
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
source.value?.clear();
e.preventDefault();
}
});
</script>
@@ -0,0 +1,75 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<ContainerEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
"<ul data-v-cf9ff940="" class="events group pt-4 medium">
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2">
<!--v-if-->
<!--v-if-->
<div data-v-961504e7="" data-v-a49e52d4="" class="tag inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] select-none" size="small">
<div class="inline-flex gap-2 whitespace-nowrap text-blue"><time datetime="2019-06-12T10:55:42.459Z" class="mobile-hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42 AM</time></div>
</div>
<div data-v-e625cddd="" data-v-a49e52d4="" class="mt-1.5 size-2.5 flex-none rounded-lg flex"></div>
<div data-v-a49e52d4="" class="log-wrapper whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap">foo bar</div>
<div data-v-a49e52d4="" class="flex gap-2 duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100">
<!--v-if-->
</div>
</div>
</li>
</ul>"
`;
exports[`<ContainerEventSource /> > render html correctly > should render dates with 24 hour style 1`] = `
"<ul data-v-cf9ff940="" class="events group pt-4 medium">
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2">
<!--v-if-->
<!--v-if-->
<div data-v-961504e7="" data-v-a49e52d4="" class="tag inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] select-none" size="small">
<div class="inline-flex gap-2 whitespace-nowrap text-blue"><time datetime="2019-06-12T10:55:42.459Z" class="mobile-hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42</time></div>
</div>
<div data-v-e625cddd="" data-v-a49e52d4="" class="mt-1.5 size-2.5 flex-none rounded-lg flex"></div>
<div data-v-a49e52d4="" class="log-wrapper whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap">foo bar</div>
<div data-v-a49e52d4="" class="flex gap-2 duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100">
<!--v-if-->
</div>
</div>
</li>
</ul>"
`;
exports[`<ContainerEventSource /> > render html correctly > should render messages 1`] = `
"<ul data-v-cf9ff940="" class="events group pt-4 medium">
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2">
<!--v-if-->
<!--v-if-->
<div data-v-961504e7="" data-v-a49e52d4="" class="tag inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] select-none" size="small">
<div class="inline-flex gap-2 whitespace-nowrap text-blue"><time datetime="2019-06-12T10:55:42.459Z" class="mobile-hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42 AM</time></div>
</div>
<div data-v-e625cddd="" data-v-a49e52d4="" class="mt-1.5 size-2.5 flex-none rounded-lg flex"></div>
<div data-v-a49e52d4="" class="log-wrapper whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap">This is a message.</div>
<div data-v-a49e52d4="" class="flex gap-2 duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100">
<!--v-if-->
</div>
</div>
</li>
</ul>"
`;
exports[`<ContainerEventSource /> > renders loading correctly 1`] = `
"<div class="flex min-h-[1px] justify-center"><span class="loading loading-bars loading-md mt-4 text-primary" style="display: none;"></span></div>
<!--v-if-->"
`;
exports[`<ContainerEventSource /> > should parse messages 1`] = `
SimpleLogEntry {
"_message": "This is a message.",
"containerID": undefined,
"date": 2019-06-12T10:55:42.459Z,
"id": 1,
"level": undefined,
"position": undefined,
"std": "stderr",
}
`;
@@ -1,114 +0,0 @@
// 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 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-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-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>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render dates with 24 hour style 1`] = `
"<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-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-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>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages 1`] = `
"<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-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-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>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with filter 1`] = `
"<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-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-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>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with html entities 1`] = `
"<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-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-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>
</li>
</ul>"
`;
exports[`<LogEventSource /> > renders correctly 1`] = `
"<div class="flex min-h-[1px] justify-center"><span class="loading loading-bars loading-md mt-4 text-primary" style="display: none;"></span></div>
<ul data-v-2e92daca="" class="events group py-4 medium"></ul>"
`;
exports[`<LogEventSource /> > should parse messages 1`] = `
SimpleLogEntry {
"_message": "This is a message.",
"date": 2019-06-12T10:55:42.459Z,
"id": 1,
"level": undefined,
"position": undefined,
"std": "stderr",
}
`;
@@ -0,0 +1,41 @@
<template>
<ScrollableView :scrollable="scrollable" v-if="containers.length && ready">
<template #header>
<div class="mx-2 flex items-center gap-2 md:ml-4">
<div class="flex flex-1 gap-1.5 truncate @container md:gap-2">
<div class="inline-flex font-mono text-sm">
<div class="font-semibold">{{ containers.length }} containers</div>
</div>
</div>
<MultiContainerStat class="ml-auto" :containers="containers" />
<MultiContainerActionToolbar class="mobile-hidden" @clear="viewer?.clear()" />
</div>
</template>
<template #default>
<ViewerWithSource
ref="viewer"
:stream-source="useMergedStream"
:entity="containers"
:visible-keys="new Map<string[], boolean>()"
:show-container-name="true"
/>
</template>
</ScrollableView>
</template>
<script lang="ts" setup>
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
import { ComponentExposed } from "vue-component-type-helpers";
const { ids = [], scrollable = false } = defineProps<{
ids?: string[];
scrollable?: boolean;
}>();
const containerStore = useContainerStore();
const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
const { allContainersById, ready } = storeToRefs(containerStore);
const containers = computed(() => ids.map((id) => allContainersById.value[id]));
provideLoggingContext(containers);
</script>
+5 -5
View File
@@ -1,12 +1,12 @@
<template>
<div class="flex flex-col gap-8 px-4 py-4 md:px-8">
<section>
<links />
<Links>
<template #more-items>
<Tag>{{ config.version }}</Tag>
</template>
</Links>
</section>
<slot></slot>
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>
+4 -5
View File
@@ -16,11 +16,10 @@
<script lang="ts" setup>
import { globalShowPopup } from "@/composable/popup";
let glopbalShow = globalShowPopup();
let show = ref(glopbalShow.value);
let delayedShow = refDebounced(show, 1000);
let content = ref<HTMLElement>();
const glopbalShow = globalShowPopup();
const show = ref(glopbalShow.value);
const delayedShow = refDebounced(show, 1000);
const content = ref<HTMLElement>();
const onMouseEnter = (e: Event) => {
show.value = true;
+2 -2
View File
@@ -7,9 +7,9 @@
{{ 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">
<Tag class="ml-auto bg-red px-1 py-1 text-xs" v-if="release.tag === latest?.tag">
{{ $t("releases.latest") }}
</tag>
</Tag>
</div>
<div class="text-sm text-base-content/80">
{{ summary(release) }}
+26 -39
View File
@@ -1,58 +1,45 @@
<template>
<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" class="fill-base-darker stroke-primary" />
</svg>
<div class="absolute inset-0 flex items-center justify-center font-light">
<template v-if="indeterminate">
<div class="text-4xl">&#8734;</div>
</template>
<template v-else-if="!isNaN(scrollProgress)">
<div class="inline-flex flex-col items-end gap-2" ref="root" v-show="!autoHide || show">
<div class="relative inline-block">
<svg width="100" height="100" viewBox="0 0 100 100" :class="{ indeterminate }">
<circle r="44" cx="50" cy="50" class="fill-base-darker stroke-primary" />
</svg>
<div class="absolute inset-0 flex items-center justify-center font-light">
<span class="text-4xl">
{{ Math.ceil(scrollProgress * 100) }}
{{ Math.ceil(progress * 100) }}
</span>
<span> % </span>
</template>
</div>
</div>
<DistanceTime :date="date" class="whitespace-nowrap text-sm" />
</div>
</transition>
</template>
<script lang="ts" setup>
const { indeterminate = false, autoHide = false } = defineProps<{
const {
indeterminate = false,
autoHide = false,
progress,
date = new Date(),
} = defineProps<{
indeterminate?: boolean;
autoHide?: boolean;
progress: number;
date?: Date;
}>();
const scrollProgress = ref(0);
const root = ref<HTMLElement>();
const store = useContainerStore();
const { activeContainers } = storeToRefs(store);
const scrollElement = ref<HTMLElement | Document>((root.value?.closest("[data-scrolling]") as HTMLElement) ?? document);
const { y: scrollY } = useScroll(scrollElement as Ref<HTMLElement | Document>, { throttle: 100 });
const show = autoResetRef(false, 2000);
onMounted(() => {
watch(
activeContainers,
() => {
scrollElement.value = (root.value?.closest("[data-scrolling]") as HTMLElement) ?? document;
},
{ immediate: true, flush: "post" },
);
});
watchPostEffect(() => {
const parent =
scrollElement.value === document
? (scrollElement.value as Document).documentElement
: (scrollElement.value as HTMLElement);
scrollProgress.value = Math.max(0, Math.min(1, scrollY.value / (parent.scrollHeight - parent.clientHeight)));
if (autoHide) {
show.value = true;
}
});
watch(
() => progress,
() => {
if (autoHide) {
show.value = true;
}
},
);
</script>
<style scoped lang="postcss">
svg {
@@ -70,7 +57,7 @@ svg {
transition: stroke-dashoffset 250ms ease-out;
transform: rotate(-90deg);
transform-origin: 50% 50%;
stroke-dashoffset: calc(276.32px - v-bind(scrollProgress) * 276.32px);
stroke-dashoffset: calc(276.32px - v-bind(progress) * 276.32px);
stroke-dasharray: 276.32px 276.32px;
stroke-linecap: round;
stroke-width: 3;
+55 -32
View File
@@ -7,23 +7,37 @@
<slot name="header"></slot>
</header>
<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 class="invisible relative md:visible" v-show="scrollContext.paused">
<div class="absolute right-44 top-4">
<ScrollProgress
:indeterminate="loadingMore"
:auto-hide="!loadingMore"
:progress="scrollContext.progress"
:date="scrollContext.currentDate"
class="!fixed z-10 min-w-40"
/>
</div>
</div>
<div ref="scrollableContent">
<slot :setLoading="setLoading"></slot>
<slot></slot>
<div v-if="scrollContext.loading" class="m-4 text-center">
<span class="loading loading-ring loading-md text-primary"></span>
</div>
</div>
<div
class="animate-background h-1 w-1/2 bg-gradient-radial from-primary to-transparent to-75%"
v-show="!scrollContext.paused && !scrollContext.loading"
></div>
<div ref="scrollObserver" class="h-px"></div>
</main>
<div class="mr-16 text-right">
<transition name="fade">
<button
class="btn btn-primary fixed bottom-8 rounded p-3 text-primary-content shadow transition-colors"
class="transition-colorsblur-xs dark btn btn-primary fixed bottom-8 rounded p-3 text-primary-content shadow"
:class="hasMore ? 'btn-secondary animate-bounce-fast text-secondary-content' : ''"
@click="scrollToBottom()"
v-show="paused"
v-show="scrollContext.paused"
>
<mdi:chevron-double-down />
</button>
@@ -35,43 +49,38 @@
<script lang="ts" setup>
const { scrollable = false } = defineProps<{ scrollable?: boolean }>();
let paused = $ref(false);
let hasMore = $ref(false);
let loading = $ref(false);
const hasMore = ref(false);
const scrollObserver = ref<HTMLElement>();
const scrollableContent = ref<HTMLElement>();
provide("scrollingPaused", $$(paused));
const scrollContext = provideScrollContext();
const mutationObserver = new MutationObserver((e) => {
if (!paused) {
scrollToBottom();
} else {
const record = e[e.length - 1];
const children = (record.target as HTMLElement).children;
if (children[children.length - 1] == record.addedNodes[record.addedNodes.length - 1]) {
hasMore = true;
}
}
});
const { loadingMore } = useLoggingContext();
const intersectionObserver = new IntersectionObserver((entries) => (paused = entries[0].intersectionRatio == 0), {
useIntersectionObserver(scrollObserver, ([entry]) => (scrollContext.paused = entry.intersectionRatio == 0), {
threshold: [0, 1],
rootMargin: "80px 0px",
rootMargin: "40px 0px",
});
onMounted(() => {
mutationObserver.observe(scrollableContent.value!, { childList: true, subtree: true });
intersectionObserver.observe(scrollObserver.value!);
});
useMutationObserver(
scrollableContent,
(records) => {
if (!scrollContext.paused) {
scrollToBottom();
} else {
const record = records[records.length - 1];
const children = (record.target as HTMLElement).children;
if (children[children.length - 1] == record.addedNodes[record.addedNodes.length - 1]) {
hasMore.value = true;
}
}
},
{ childList: true, subtree: true },
);
function scrollToBottom(behavior: "auto" | "smooth" = "auto") {
scrollObserver.value?.scrollIntoView({ behavior });
hasMore = false;
}
function setLoading(value: boolean) {
loading = value;
hasMore.value = false;
}
</script>
<style scoped lang="postcss">
@@ -84,6 +93,20 @@ function setLoading(value: boolean) {
.fade-leave-to {
@apply opacity-0;
}
.animate-background {
animation: gradient-animation 3s ease-out infinite;
}
@keyframes gradient-animation {
0%,
100% {
transform: translateX(0%);
}
50% {
transform: translateX(100%);
}
}
</style>
<style>
+7 -4
View File
@@ -7,14 +7,17 @@
ref="container"
:style="style"
>
<div class="input input-primary flex h-auto items-center !shadow-lg">
<div
class="input input-primary flex h-auto items-center !shadow-lg"
:class="!isValidQuery ? 'input-warning' : ''"
>
<mdi:magnify />
<input
class="input input-ghost w-72 flex-1"
type="text"
placeholder="Find / RegEx"
ref="input"
v-model="searchFilter"
v-model="searchQueryFilter"
@keyup.esc="resetSearch()"
/>
<a class="btn btn-circle btn-xs" @click="resetSearch()"> <mdi:close /></a>
@@ -26,12 +29,12 @@
<script lang="ts" setup>
const input = ref<HTMLInputElement>();
const container = ref<HTMLDivElement>();
const { searchFilter, showSearch, resetSearch } = useSearchFilter();
const { searchQueryFilter, showSearch, resetSearch, isValidQuery } = useSearchFilter();
const { style } = useDraggable(container);
onKeyStroke("f", (e) => {
if (e.ctrlKey || e.metaKey) {
if ((e.ctrlKey || e.metaKey) && !e.shiftKey) {
showSearch.value = true;
nextTick(() => input.value?.focus() || input.value?.select());
e.preventDefault();
@@ -0,0 +1,45 @@
<template>
<ScrollableView :scrollable="scrollable" v-if="service.name">
<template #header>
<div class="mx-2 flex items-center gap-2 md:ml-4">
<div class="flex flex-1 gap-1.5 truncate @container md:gap-2">
<div class="inline-flex font-mono text-sm">
<div class="font-semibold">{{ service.name }}</div>
</div>
<Tag class="mobile-hidden hidden font-mono @3xl:block" size="small">
{{ $t("label.container", service.containers.length) }}
</Tag>
</div>
<MultiContainerStat class="ml-auto" :containers="service.containers" />
<MultiContainerActionToolbar class="mobile-hidden" @clear="viewer?.clear()" />
</div>
</template>
<template #default>
<ViewerWithSource
ref="viewer"
:stream-source="useServiceStream"
:entity="service"
:visible-keys="new Map<string[], boolean>()"
:show-container-name="true"
/>
</template>
</ScrollableView>
</template>
<script lang="ts" setup>
import { Service } from "@/models/Stack";
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
import { ComponentExposed } from "vue-component-type-helpers";
const { name, scrollable = false } = defineProps<{
scrollable?: boolean;
name: string;
}>();
const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
const store = useSwarmStore();
const { services } = storeToRefs(store) as unknown as { services: Ref<Service[]> };
const service = computed(() => services.value.find((s) => s.name === name) ?? new Service("", []));
provideLoggingContext(toRef(() => service.value.containers));
</script>
+23 -212
View File
@@ -1,63 +1,17 @@
<template>
<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>
</div>
<Toggle v-model="showSwarm" v-if="services.length > 0 || customGroups.length > 0">
<div class="text-lg font-light">{{ $t("label.swarm-mode") }}</div>
</Toggle>
<transition :name="sessionHost ? 'slide-left' : 'slide-right'" mode="out-in">
<ul class="menu p-0" v-if="!sessionHost">
<li v-for="host in hosts" :key="host.id">
<a @click.prevent="setHost(host.id)" :class="{ 'pointer-events-none text-base-content/50': !host.available }">
<ph:computer-tower />
{{ host.name }}
<span class="badge badge-error badge-xs p-1.5" v-if="!host.available">offline</span>
</a>
</li>
</ul>
<ul class="containers menu p-0 [&_li.menu-title]:px-0" v-else>
<li v-for="{ label, containers, icon } in menuItems" :key="label">
<details :open="!collapsedGroups.has(label)" @toggle="updateCollapsedGroups($event, label)">
<summary class="font-light text-base-content/80">
<component :is="icon" />
{{ label.startsWith("label.") ? $t(label) : label }}
</summary>
<ul>
<li v-for="item in containers" :class="item.state" :key="item.id">
<popup>
<router-link
:to="{ name: 'container-id', params: { id: item.id } }"
active-class="active-primary"
@click.alt.stop.prevent="store.appendActiveContainer(item)"
:title="item.name"
>
<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>
</li>
</ul>
</details>
</li>
</ul>
</transition>
<SlideTransition :slide-right="showSwarm">
<template #left>
<HostMenu />
</template>
<template #right>
<SwarmMenu />
</template>
</SlideTransition>
</div>
<div role="status" class="flex animate-pulse flex-col gap-4" v-else>
<div class="h-3 w-full rounded-full bg-base-content/50 opacity-50" v-for="_ in 9"></div>
@@ -66,164 +20,21 @@
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
import { sessionHost } from "@/composable/storage";
import { onBeforeRouteLeave } from "vue-router";
const containerStore = useContainerStore();
const { ready } = storeToRefs(containerStore);
// @ts-ignore
import Pin from "~icons/ph/map-pin-simple";
// @ts-ignore
import Stack from "~icons/ph/stack";
// @ts-ignore
import Containers from "~icons/octicon/container-24";
const swarmStore = useSwarmStore();
const { services, customGroups } = storeToRefs(swarmStore);
const store = useContainerStore();
const showSwarm = useSessionStorage<boolean>("DOZZLE_SWARM_MODE", false);
const { activeContainers, visibleContainers, ready } = storeToRefs(store);
const { hosts } = useHosts();
const setHost = (host: string | null) => (sessionHost.value = host);
const collapsedGroups = useProfileStorage("collapsedGroups", new Set<string>());
const updateCollapsedGroups = (event: Event, label: string) => {
const details = event.target as HTMLDetailsElement;
if (details.open) {
collapsedGroups.value.delete(label);
} else {
collapsedGroups.value.add(label);
onBeforeRouteLeave((to) => {
if (to.meta.swarmMode) {
showSwarm.value = true;
} else if (to.meta.containerMode) {
showSwarm.value = false;
}
};
const debouncedPinnedContainers = debouncedRef(pinnedContainers, 200);
const sortedContainers = computed(() =>
visibleContainers.value.filter((c) => c.host === sessionHost.value).sort(sorter),
);
const sorter = (a: Container, b: Container) => {
if (a.state === "running" && b.state !== "running") {
return -1;
} else if (a.state !== "running" && b.state === "running") {
return 1;
} else {
return a.name.localeCompare(b.name);
}
};
const menuItems = computed(() => {
const namespaced: Record<string, Container[]> = {};
const pinned = [];
const singular = [];
for (const item of sortedContainers.value) {
const namespace = item.labels["com.docker.stack.namespace"] ?? item.labels["com.docker.compose.project"];
if (debouncedPinnedContainers.value.has(item.name)) {
pinned.push(item);
} else if (namespace) {
namespaced[namespace] ||= [];
namespaced[namespace].push(item);
} else {
singular.push(item);
}
}
const items = [];
if (pinned.length) {
items.push({ label: "label.pinned", containers: pinned, icon: Pin });
}
for (const [label, containers] of Object.entries(namespaced).sort(([a], [b]) => a.localeCompare(b))) {
if (containers.length > 1) {
items.push({ label, containers, icon: Stack });
} else {
singular.push(containers[0]);
}
}
singular.sort(sorter);
if (singular.length) {
items.push({
label: showAllContainers.value ? "label.all-containers" : "label.running-containers",
containers: singular,
icon: Containers,
});
}
return items;
});
const activeContainersById = computed(() =>
activeContainers.value.reduce(
(acc, item) => {
acc[item.id] = item;
return acc;
},
{} as Record<string, Container>,
),
);
</script>
<style scoped lang="postcss">
.menu {
@apply text-[0.95rem];
}
.containers a {
@apply auto-cols-[auto_max-content_max-content];
.pin {
display: none;
&:hover {
@apply text-secondary;
}
}
&:hover {
.pin {
display: inline-block;
}
}
}
li.exited {
@apply opacity-50;
}
.slide-left-enter-active,
.slide-left-leave-active,
.slide-right-enter-active,
.slide-right-leave-active {
transition: all 0.1s ease-out;
}
.slide-left-enter-from {
opacity: 0;
transform: translateX(20px);
}
.slide-right-enter-from {
opacity: 0;
transform: translateX(-20px);
}
.slide-left-leave-to {
opacity: 0;
transform: translateX(-20px);
}
.slide-right-leave-to {
opacity: 0;
transform: translateX(20px);
}
.list-move,
.list-enter-active,
.list-leave-active {
transition: all 0.19s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
.list-leave-active {
position: absolute;
}
</style>
<style scoped lang="postcss"></style>
+15 -4
View File
@@ -1,10 +1,8 @@
<template>
<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 :to="{ name: '/' }">
<LogoWithText class="logo h-16 w-40" />
</router-link>
<small class="mb-4 block text-xs font-light" v-if="hostname">
@@ -28,5 +26,18 @@
</template>
<script lang="ts" setup>
import LogoWithText from "@/logo-text.svg";
const { hostname } = config;
</script>
<style scoped lang="postcss">
.logo {
:deep(.content-fill) {
@apply fill-base-content;
}
:deep(.secondary-fill) {
@apply fill-secondary;
}
}
</style>
@@ -0,0 +1,46 @@
<template>
<ScrollableView :scrollable="scrollable" v-if="stack.name">
<template #header>
<div class="mx-2 flex items-center gap-2 md:ml-4">
<div class="flex flex-1 gap-1.5 truncate @container md:gap-2">
<div class="inline-flex font-mono text-sm">
<div class="font-semibold">{{ stack.name }}</div>
</div>
<Tag class="mobile-hidden hidden font-mono @3xl:block" size="small">
{{ $t("label.container", stack.containers.length) }}
</Tag>
<Tag class="mobile-hidden hidden font-mono @3xl:block" size="small">
{{ $t("label.serivce", stack.services.length) }}
</Tag>
</div>
<MultiContainerStat class="ml-auto" :containers="stack.containers" />
<MultiContainerActionToolbar class="mobile-hidden" @clear="viewer?.clear()" />
</div>
</template>
<template #default>
<ViewerWithSource
ref="viewer"
:stream-source="useStackStream"
:entity="stack"
:visible-keys="new Map<string[], boolean>()"
:show-container-name="true"
/>
</template>
</ScrollableView>
</template>
<script lang="ts" setup>
import { Stack } from "@/models/Stack";
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
import { ComponentExposed } from "vue-component-type-helpers";
const { name, scrollable = false } = defineProps<{
scrollable?: boolean;
name: string;
}>();
const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
const store = useSwarmStore();
const { stacks } = storeToRefs(store) as unknown as { stacks: Ref<Stack[]> };
const stack = computed(() => stacks.value.find((s) => s.name === name) ?? new Stack("", [], []));
provideLoggingContext(toRef(() => stack.value.containers));
</script>
+88
View File
@@ -0,0 +1,88 @@
<template>
<ul class="menu p-0 text-[0.95rem]">
<li v-for="{ name, services } in stacks" :key="name">
<details open>
<summary class="font-light text-base-content/80">
<ph:stack />
{{ name }}
<router-link
:to="{ name: '/stack/[name]', params: { name } }"
class="btn btn-square btn-outline btn-primary btn-xs"
active-class="btn-active"
:title="$t('tooltip.merge-services')"
>
<ph:arrows-merge />
</router-link>
</summary>
<ul>
<li v-for="service in services" :key="service.name">
<router-link
:to="{ name: '/service/[name]', params: { name: service.name } }"
active-class="active-primary"
>
<ph:stack-simple />
<div class="truncate">
{{ service.name }}
</div>
</router-link>
</li>
</ul>
</details>
</li>
<li v-if="serivcesWithoutStacks.length > 0">
<details open>
<summary class="font-light text-base-content/80">
<ph:circles-four />
{{ $t("label.services") }}
</summary>
<ul>
<li v-for="service in serivcesWithoutStacks" :key="service.name">
<router-link
:to="{ name: '/service/[name]', params: { name: service.name } }"
active-class="active-primary"
>
<ph:stack-simple />
<div class="truncate">
{{ service.name }}
</div>
</router-link>
</li>
</ul>
</details>
</li>
<li v-if="customGroups.length > 0">
<details open>
<summary class="font-light text-base-content/80">
<ph:bounding-box-fill />
{{ $t("label.custom-groups") }}
</summary>
<ul>
<li v-for="group in customGroups" :key="group.name">
<router-link :to="{ name: '/group/[name]', params: { name: group.name } }" active-class="active-primary">
<ph:stack-simple />
<div class="truncate">
{{ group.name }}
</div>
</router-link>
</li>
</ul>
</details>
</li>
</ul>
</template>
<script lang="ts" setup>
const store = useSwarmStore();
const { stacks, services, customGroups } = storeToRefs(store);
const serivcesWithoutStacks = computed(() => services.value.filter((service) => !service.stack));
</script>
<style scoped lang="postcss">
.menu {
@apply text-[0.95rem];
}
</style>
+10 -8
View File
@@ -6,7 +6,7 @@
<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">
<a @click="update(item.value as T)">
<mdi:check class="w-4" v-if="modelValue == item.value" />
<div v-else class="w-4"></div>
{{ item.label }}
@@ -24,17 +24,19 @@ type DropdownItem = {
value: T;
};
const { modelValue } = defineModels<{
modelValue: T;
}>();
const model = defineModel<T>();
const { options = [], defaultLabel = "" } = defineProps<{
options?: DropdownItem[];
const { options, defaultLabel = "" } = defineProps<{
options: DropdownItem[];
defaultLabel?: string;
}>();
const label = computed(() => options.find((item) => item.value === modelValue.value)?.label ?? defaultLabel);
const label = computed(() => options.find((item) => item.value === model.value)?.label ?? defaultLabel);
const details = ref<HTMLElement | null>(null);
const close = () => details.value?.removeAttribute("open");
watch(modelValue, () => close());
const update = (value: T) => {
model.value = value;
close();
};
</script>
+10
View File
@@ -0,0 +1,10 @@
<template>
<mdi:satellite-variant v-if="type == 'agent'" />
<ph:globe-simple v-else-if="type == 'remote'" />
<mdi:hexagon-multiple v-else-if="type == 'swarm'" />
<ph:computer-tower v-else />
</template>
<script setup lang="ts">
import { Host } from "@/stores/hosts";
const { type } = defineProps<{ type: Host["type"] }>();
</script>
+10 -47
View File
@@ -1,10 +1,8 @@
<template>
<nav class="fixed top-0 z-10 w-full border-b border-base-content/20 bg-base p-2" data-testid="navigation">
<div class="flex items-center">
<router-link :to="{ name: 'index' }">
<svg class="h-14 w-28 fill-secondary">
<use href="#logo"></use>
</svg>
<router-link :to="{ name: '/' }">
<Logo class="logo h-8" />
</router-link>
<div class="ml-auto flex items-center gap-2">
@@ -21,61 +19,20 @@
<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>
<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>
<SideMenu />
</div>
</transition>
</nav>
</template>
<script lang="ts" setup>
import { sessionHost } from "@/composable/storage";
const store = useContainerStore();
import Logo from "@/logo.svg";
const route = useRoute();
const { visibleContainers } = storeToRefs(store);
const show = ref(false);
watch(route, () => {
show.value = false;
});
const sortedContainers = computed(() =>
visibleContainers.value
.filter((c) => c.host === sessionHost.value)
.sort((a, b) => {
if (a.state === "running" && b.state !== "running") {
return -1;
} else if (a.state !== "running" && b.state === "running") {
return 1;
} else {
return a.name.localeCompare(b.name);
}
}),
);
const hosts = computed(() => config.hosts.map(({ id, name }) => ({ value: id, label: name })));
</script>
<style scoped lang="postcss">
li.exited {
@@ -101,4 +58,10 @@ li.exited {
.fade-leave-to .menu {
@apply -translate-y-2;
}
.logo {
:deep(.secondary-fill) {
@apply fill-secondary;
}
}
</style>
+51
View File
@@ -0,0 +1,51 @@
<template>
<dialog ref="panel" class="modal-right modal items-start outline-none backdrop:bg-none">
<div class="modal-box" :width="width">
<form method="dialog">
<button class="swap swap-rotate absolute right-4 top-4 outline-none hover:swap-active">
<mdi:keyboard-esc class="swap-off" />
<mdi:close class="swap-on" />
</button>
</form>
<slot v-if="open"></slot>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
</template>
<script setup lang="ts">
import { type DrawerWidth } from "@/composable/drawer";
const panel = useTemplateRef<HTMLDialogElement>("panel");
const open = ref(false);
const { width } = defineProps<{
width: DrawerWidth;
}>();
defineExpose({
open: () => {
open.value = true;
panel.value?.showModal();
},
});
useEventListener(panel, "close", () => (open.value = false));
</script>
<style scoped lang="postcss">
.modal-right :where(.modal-box) {
@apply fixed right-0 h-lvh max-h-screen translate-x-24 scale-100 rounded-none bg-base-lighter shadow-none;
&[width="md"] {
@apply max-w-3xl;
}
&[width="lg"] {
@apply max-w-5xl;
}
}
.modal-right[open] .modal-box {
@apply translate-x-0;
}
</style>
@@ -0,0 +1,42 @@
<template>
<transition :name="!slideRight ? 'slide-left' : 'slide-right'" mode="out-in">
<div v-if="!slideRight">
<slot name="left" />
</div>
<div v-else>
<slot name="right" />
</div>
</transition>
</template>
<script lang="ts" setup>
const { slideRight } = defineProps<{ slideRight: boolean }>();
</script>
<style scoped lang="postcss">
.slide-left-enter-active,
.slide-left-leave-active,
.slide-right-enter-active,
.slide-right-leave-active {
transition: all 0.1s ease-out;
}
.slide-left-enter-from {
opacity: 0;
transform: translateX(20px);
}
.slide-right-enter-from {
opacity: 0;
transform: translateX(-20px);
}
.slide-left-leave-to {
opacity: 0;
transform: translateX(-20px);
}
.slide-right-leave-to {
opacity: 0;
transform: translateX(20px);
}
</style>
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<div class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] text-sm" :size="size">
<div class="tag inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em]" :size="size">
<slot></slot>
</div>
</template>
+6 -2
View File
@@ -1,7 +1,7 @@
<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">
<div>
<slot></slot>
</div>
</button>
@@ -13,9 +13,13 @@ const finished = defineEmit();
const cancelled = defineEmit();
let animation: Animation | undefined;
const { duration = 4000 } = defineProps<{
duration?: number;
}>();
onMounted(async () => {
animation = progress.value?.animate([{ transform: "scaleX(0)" }, { transform: "scaleX(1)" }], {
duration: 4000,
duration: duration,
easing: "linear",
fill: "forwards",
});
+3 -3
View File
@@ -1,12 +1,12 @@
<template>
<labeled-input>
<LabeledInput>
<template #label>
<slot />
</template>
<template #input>
<input type="checkbox" class="toggle toggle-primary" v-model="modelValue" />
<input type="checkbox" class="toggle toggle-primary" v-model="modelValue" v-bind="$attrs" />
</template>
</labeled-input>
</LabeledInput>
</template>
<script lang="ts" setup>
+3 -2
View File
@@ -1,6 +1,7 @@
import { Container } from "@/models/Container";
type ContainerActions = "start" | "stop" | "restart";
export const useContainerActions = () => {
const { container } = useContainerContext();
export const useContainerActions = (container: Ref<Container>) => {
const { showToast } = useToast();
const actionStates = reactive({
-23
View File
@@ -1,23 +0,0 @@
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;
};
+29
View File
@@ -0,0 +1,29 @@
import SideDrawer from "@/components/common/SideDrawer.vue";
import { Component } from "vue";
export type DrawerWidth = "md" | "xl" | "lg";
export const drawerContext = Symbol("drawer") as InjectionKey<
(c: Component, p: Record<string, any>, s?: DrawerWidth) => void
>;
export const createDrawer = (drawer: Ref<InstanceType<typeof SideDrawer>>) => {
const component = shallowRef<Component | null>(null);
const properties = shallowRef<Record<string, any>>({});
const width = ref<DrawerWidth>("md");
const showDrawer = (c: Component, p: Record<string, any>, w: DrawerWidth = "md") => {
component.value = c;
properties.value = p;
width.value = w;
drawer.value?.open();
};
provide(drawerContext, showDrawer);
return { component, properties, showDrawer, width };
};
export const useDrawer = () =>
inject(drawerContext, () => {
console.error("No drawer context provided");
});
+30
View File
@@ -0,0 +1,30 @@
import * as duckdb from "@duckdb/duckdb-wasm";
const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles();
export async function useDuckDB() {
let cleanup: (() => void) | undefined;
onUnmounted(() => cleanup?.());
const bundle = await duckdb.selectBundle(JSDELIVR_BUNDLES);
const worker_url = URL.createObjectURL(
new Blob([`importScripts("${bundle.mainWorker!}");`], { type: "text/javascript" }),
);
// Instantiate the asynchronus version of DuckDB-Wasm
const worker = new Worker(worker_url);
const logger = new duckdb.ConsoleLogger();
const db = new duckdb.AsyncDuckDB(logger, worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
URL.revokeObjectURL(worker_url);
const conn = await db.connect();
cleanup = async () => {
console.log("Cleaning up DuckDB");
await conn.close();
await db.terminate();
worker.terminate();
};
return { db, conn };
}
+207
View File
@@ -0,0 +1,207 @@
import { ShallowRef, type Ref } from "vue";
import debounce from "lodash.debounce";
import {
type LogEvent,
type JSONObject,
LogEntry,
asLogEntry,
ContainerEventLogEntry,
ComplexLogEntry,
SkippedLogsEntry,
} from "@/models/LogEntry";
import { Service, Stack } from "@/models/Stack";
import { Container, GroupedContainers } from "@/models/Container";
const { isSearching, debouncedSearchFilter } = useSearchFilter();
function parseMessage(data: string): LogEntry<string | JSONObject> {
const e = JSON.parse(data) as LogEvent;
return asLogEntry(e);
}
export function useContainerStream(container: Ref<Container>): LogStreamSource {
const url = computed(() => `/api/hosts/${container.value.host}/containers/${container.value.id}/logs/stream`);
const loadMoreUrl = computed(() => `/api/hosts/${container.value.host}/containers/${container.value.id}/logs`);
return useLogStream(url, loadMoreUrl);
}
export function useStackStream(stack: Ref<Stack>): LogStreamSource {
return useLogStream(computed(() => `/api/stacks/${stack.value.name}/logs/stream`));
}
export function useGroupedStream(group: Ref<GroupedContainers>): LogStreamSource {
return useLogStream(computed(() => `/api/groups/${group.value.name}/logs/stream`));
}
export function useMergedStream(containers: Ref<Container[]>): LogStreamSource {
const url = computed(() => {
const ids = containers.value.map((c) => c.id).join(",");
return `/api/hosts/${containers.value[0].host}/logs/mergedStream/${ids}`;
});
return useLogStream(url);
}
export function useServiceStream(service: Ref<Service>): LogStreamSource {
return useLogStream(computed(() => `/api/services/${service.value.name}/logs/stream`));
}
export type LogStreamSource = ReturnType<typeof useLogStream>;
function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
const messages: ShallowRef<LogEntry<string | JSONObject>[]> = shallowRef([]);
const buffer: ShallowRef<LogEntry<string | JSONObject>[]> = shallowRef([]);
const { paused: scrollingPaused } = useScrollContext();
function flushNow() {
if (messages.value.length + buffer.value.length > config.maxLogs) {
if (scrollingPaused.value === true) {
if (messages.value.at(-1) instanceof SkippedLogsEntry) {
const lastEvent = messages.value.at(-1) as SkippedLogsEntry;
const lastItem = buffer.value.at(-1) as LogEntry<string | JSONObject>;
lastEvent.addSkippedEntries(buffer.value.length, lastItem);
} else {
const firstItem = buffer.value.at(0) as LogEntry<string | JSONObject>;
const lastItem = buffer.value.at(-1) as LogEntry<string | JSONObject>;
messages.value = [
...messages.value,
new SkippedLogsEntry(new Date(), buffer.value.length, firstItem, lastItem),
];
}
buffer.value = [];
} else {
if (buffer.value.length > config.maxLogs / 2) {
messages.value = buffer.value.slice(-config.maxLogs / 2);
} else {
messages.value = [...messages.value, ...buffer.value].slice(-config.maxLogs);
}
buffer.value = [];
}
} else {
if (messages.value.length == 0) {
// sort the buffer the very first time because of multiple logs in parallel
buffer.value.sort((a, b) => a.date.getTime() - b.date.getTime());
}
messages.value = [...messages.value, ...buffer.value];
buffer.value = [];
}
}
const flushBuffer = debounce(flushNow, 250, { maxWait: 1000 });
let es: EventSource | null = null;
function close() {
if (es) {
es.close();
es = null;
}
}
function clearMessages() {
flushBuffer.cancel();
messages.value = [];
buffer.value = [];
}
const { streamConfig, hasComplexLogs, levels } = useLoggingContext();
const params = computed(() => {
const params = new URLSearchParams();
if (streamConfig.value.stdout) params.append("stdout", "1");
if (streamConfig.value.stderr) params.append("stderr", "1");
if (isSearching.value) params.append("filter", debouncedSearchFilter.value);
for (const level of levels.value) {
params.append("levels", level);
}
return params;
});
const urlWithParams = computed(() => withBase(`${url.value}?${params.value.toString()}`));
function connect({ clear } = { clear: true }) {
close();
if (clear) clearMessages();
es = new EventSource(urlWithParams.value);
es.addEventListener("container-event", (e) => {
const event = JSON.parse((e as MessageEvent).data) as { actorId: string; name: string };
const containerEvent = new ContainerEventLogEntry(
event.name == "container-started" ? "Container started" : "Container stopped",
event.actorId,
new Date(),
event.name as "container-stopped" | "container-started",
);
buffer.value = [...buffer.value, containerEvent];
flushBuffer.flush();
});
es.addEventListener("logs-backfill", (e) => {
const data = JSON.parse((e as MessageEvent).data) as LogEvent[];
const logs = data.map((e) => asLogEntry(e));
messages.value = [...logs, ...messages.value];
});
es.onmessage = (e) => {
if (e.data) {
buffer.value = [...buffer.value, parseMessage(e.data)];
flushBuffer();
}
};
es.onerror = () => clearMessages();
}
watch(urlWithParams, () => connect(), { immediate: true });
const isLoadingMore = ref(false);
async function loadOlderLogs() {
if (!loadMoreUrl) return;
if (isLoadingMore.value) return;
const to = messages.value[0].date;
const last = messages.value[Math.min(messages.value.length - 1, 300)].date;
const delta = to.getTime() - last.getTime();
const from = new Date(to.getTime() + delta);
const abortController = new AbortController();
const signal = abortController.signal;
isLoadingMore.value = true;
try {
const urlWithMoreParams = computed(() => {
const loadMoreParams = new URLSearchParams(params.value);
loadMoreParams.append("from", from.toISOString());
loadMoreParams.append("to", to.toISOString());
loadMoreParams.append("minimum", "100");
return withBase(`${loadMoreUrl.value}?${loadMoreParams.toString()}`);
});
const stopWatcher = watchOnce(urlWithMoreParams, () => abortController.abort("stream changed"));
const logs = await (await fetch(urlWithMoreParams.value, { signal })).text();
stopWatcher();
if (logs && signal.aborted === false) {
const newMessages = logs
.trim()
.split("\n")
.map((line) => parseMessage(line));
messages.value = [...newMessages, ...messages.value];
}
} catch (e) {
console.error("Error loading older logs", e);
} finally {
isLoadingMore.value = false;
}
}
onScopeDispose(() => close());
watch(messages, () => {
if (messages.value.length > 1) {
hasComplexLogs.value = messages.value.some((m) => m instanceof ComplexLogEntry);
}
});
return { messages, loadOlderLogs, isLoadingMore, hasComplexLogs };
}
-160
View File
@@ -1,160 +0,0 @@
import { type Ref } from "vue";
import { encodeXML } from "entities";
import debounce from "lodash.debounce";
import {
type LogEvent,
type JSONObject,
LogEntry,
asLogEntry,
DockerEventLogEntry,
SkippedLogsEntry,
} from "@/models/LogEntry";
function parseMessage(data: string): LogEntry<string | JSONObject> {
const e = JSON.parse(data, (key, value) => {
if (typeof value === "string") {
return encodeXML(value);
}
return value;
}) as LogEvent;
return asLogEntry(e);
}
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) {
if (scrollingPaused) {
console.log("Skipping ", buffer.length, " log items");
if (messages.at(-1) instanceof SkippedLogsEntry) {
const lastEvent = messages.at(-1) as SkippedLogsEntry;
const lastItem = buffer.at(-1) as LogEntry<string | JSONObject>;
lastEvent.addSkippedEntries(buffer.length, lastItem);
} else {
const firstItem = buffer.at(0) as LogEntry<string | JSONObject>;
const lastItem = buffer.at(-1) as LogEntry<string | JSONObject>;
messages.push(new SkippedLogsEntry(new Date(), buffer.length, firstItem, lastItem));
}
buffer = [];
} else {
messages.push(...buffer);
buffer = [];
messages = messages.slice(-config.maxLogs);
}
} else {
messages.push(...buffer);
buffer = [];
}
}
const flushBuffer = debounce(flushNow, 250, { maxWait: 1000 });
let es: EventSource | null = null;
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 }) {
close();
if (clear) {
clearMessages();
}
const params = Object.entries(streamConfig)
.filter(([, value]) => value)
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), {});
containerId = container.value.id;
console.debug(`Connecting to ${containerId} with params`, params);
es = new EventSource(
withBase(
`/api/hosts/${container.value.host}/containers/${containerId}/logs/stream?${new URLSearchParams(params).toString()}`,
),
);
es.addEventListener("container-stopped", () => {
close();
buffer.push(new DockerEventLogEntry("Container stopped", new Date(), "container-stopped"));
flushBuffer();
flushBuffer.flush();
});
es.onmessage = (e) => {
if (e.data) {
buffer.push(parseMessage(e.data));
flushBuffer();
}
};
es.onerror = () => clearMessages();
}
async function loadOlderLogs({ beforeLoading, afterLoading } = { beforeLoading: () => {}, afterLoading: () => {} }) {
if (messages.length < 300) return;
beforeLoading();
const to = messages[0].date;
const last = messages[299].date;
const delta = to.getTime() - last.getTime();
const from = new Date(to.getTime() + delta);
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(
withBase(
`/api/hosts/${container.value.host}/containers/${containerId}/logs?${new URLSearchParams(params).toString()}`,
),
)
).text();
if (logs) {
const newMessages = logs
.trim()
.split("\n")
.map((line) => parseMessage(line));
messages.unshift(...newMessages);
}
afterLoading();
}
watch(
() => container.value.state,
(newValue, oldValue) => {
console.log("LogEventSource: container changed", newValue, oldValue);
if (newValue == "running" && newValue != oldValue) {
buffer.push(new DockerEventLogEntry("Container started", new Date(), "container-started"));
connect({ clear: false });
}
},
);
onUnmounted(() => close());
watch(
() => container.value.id,
() => connect(),
{ immediate: true },
);
watch(streamConfig, () => connect());
return { ...$$({ messages }), loadOlderLogs };
}
+56
View File
@@ -0,0 +1,56 @@
import { Container } from "@/models/Container";
import { Level } from "@/models/LogEntry";
type LogContext = {
streamConfig: { stdout: boolean; stderr: boolean };
containers: Container[];
loadingMore: boolean;
hasComplexLogs: boolean;
levels: Set<Level>;
};
export const allLevels: Level[] = [
"info",
"debug",
"warn",
"error",
"fatal",
"trace",
"warning",
"critical",
"unknown",
];
// export for testing
export const loggingContextKey = Symbol("loggingContext") as InjectionKey<LogContext>;
const searchParams = new URLSearchParams(window.location.search);
const stdout = searchParams.has("stdout") ? searchParams.get("stdout") === "true" : true;
const stderr = searchParams.has("stderr") ? searchParams.get("stderr") === "true" : true;
export const provideLoggingContext = (containers: Ref<Container[]>) => {
provide(
loggingContextKey,
reactive({
streamConfig: { stdout, stderr },
containers,
loadingMore: false,
hasComplexLogs: false,
levels: new Set<Level>(allLevels),
}),
);
};
export const useLoggingContext = () => {
const context = inject(
loggingContextKey,
reactive({
streamConfig: { stdout: true, stderr: true },
containers: [],
loadingMore: false,
hasComplexLogs: false,
levels: new Set<Level>(allLevels),
}),
);
return toRefs(context);
};
-16
View File
@@ -1,16 +0,0 @@
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,
};
};
+33 -3
View File
@@ -1,14 +1,34 @@
import { Profile } from "@/stores/config";
export function useProfileStorage<K extends keyof Profile>(key: K, defaultValue: NonNullable<Profile[K]>) {
interface SerializerTransformer<T, U> {
from: (raw: U) => T;
to: (value: T) => U;
}
export function useProfileStorage<K extends keyof Profile>(
key: K,
defaultValue: NonNullable<Profile[K]>,
transformer?: SerializerTransformer<NonNullable<Profile[K]>, any>,
) {
const storageKey = "DOZZLE_" + key.toUpperCase();
const storage = useStorage<NonNullable<Profile[K]>>(storageKey, defaultValue, undefined, {
writeDefaults: false,
mergeDefaults: true,
serializer: transformer
? {
read: (raw) => transformer.from(JSON.parse(raw)),
write: (value) => JSON.stringify(transformer.to(value)),
}
: undefined,
onError: (e) => {
console.error(`Failed to read ${storageKey} from storage`, e);
},
});
if (config.profile?.[key]) {
if (storage.value instanceof Set && config.profile[key] instanceof Array) {
if (transformer) {
storage.value = transformer.from(config.profile[key]);
} else 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]>;
@@ -23,9 +43,19 @@ export function useProfileStorage<K extends keyof Profile>(key: K, defaultValue:
watch(
storage,
(value) => {
const transformedValue = transformer ? transformer.to(value) : value;
fetch(withBase("/api/profile"), {
method: "PATCH",
body: JSON.stringify({ [key]: value }, (_, value) => (value instanceof Set ? [...value] : value)),
body: JSON.stringify({ [key]: transformedValue }, (_, value) => {
if (value instanceof Set) {
return Array.from(value);
} else if (value instanceof Map) {
return Array.from(value.entries());
} else {
return value;
}
}),
});
},
{ deep: true },
+30
View File
@@ -0,0 +1,30 @@
type ScrollContext = {
loading: boolean;
paused: boolean;
progress: number;
currentDate: Date;
};
// export for testing
export const scrollContextKey = Symbol("scrollContext") as InjectionKey<ScrollContext>;
export const provideScrollContext = () => {
const context = defauleValue();
provide(scrollContextKey, context);
return context;
};
export const useScrollContext = () => {
const defaultValue = defauleValue();
const context = inject(scrollContextKey, defaultValue);
return toRefs(context);
};
function defauleValue() {
return reactive({
loading: false,
paused: false,
progress: 1,
currentDate: new Date(),
});
}
+24 -75
View File
@@ -1,85 +1,34 @@
import { type Ref } from "vue";
import { type LogEntry, type JSONObject, SimpleLogEntry, ComplexLogEntry } from "@/models/LogEntry";
import { encodeXML } from "entities";
const searchFilter = ref<string>("");
const debouncedSearchFilter = useDebounce(searchFilter);
const searchQueryFilter = ref<string>("");
const debouncedSearchFilter = refDebounced(searchQueryFilter);
const showSearch = ref(false);
function matchRecord(record: Record<string, any>, regex: RegExp): boolean {
for (const key in record) {
const value = record[key];
if (typeof value === "string" && regex.test(value)) {
return true;
}
if (isObject(value) && regex.test(JSON.stringify(value))) {
return true;
}
if (Array.isArray(value) && matchRecord(value, regex)) {
return true;
}
}
return false;
const searchParams = new URLSearchParams(window.location.search);
if (searchParams.get("search") !== null && searchParams.get("search") !== "") {
searchQueryFilter.value = searchParams.get("search") || "";
showSearch.value = true;
}
function resetSearch() {
searchQueryFilter.value = "";
showSearch.value = false;
}
const isSearching = computed(() => showSearch.value && debouncedSearchFilter.value !== "");
const isValidQuery = computed(() => {
try {
new RegExp(searchQueryFilter.value);
return true;
} catch (e) {
return false;
}
});
export function useSearchFilter() {
const regex = $computed(() => {
const isSmartCase = debouncedSearchFilter.value === debouncedSearchFilter.value.toLowerCase();
return new RegExp(encodeXML(debouncedSearchFilter.value), isSmartCase ? "ig" : "g");
});
function filteredMessages(messages: Ref<LogEntry<string | JSONObject>[]>) {
return computed(() => {
if (debouncedSearchFilter.value && showSearch.value) {
try {
return messages.value.filter((d) => {
if (d instanceof SimpleLogEntry) {
return regex.test(d.message);
} else if (d instanceof ComplexLogEntry) {
return matchRecord(d.message, regex);
}
return false;
});
} catch (e) {
if (e instanceof SyntaxError) {
console.info(`Ignoring SyntaxError from search.`, e);
return messages.value;
}
throw e;
}
}
return messages.value;
});
}
function markSearch(log: { toString(): string }): string;
function markSearch(log: string[]): string[];
function markSearch(log: { toString(): string } | string[]) {
if (!debouncedSearchFilter.value) {
return log;
}
if (Array.isArray(log)) {
return log.map((d) => markSearch(d));
}
return log.toString().replace(regex, (match) => `<mark>${match}</mark>`);
}
function resetSearch() {
searchFilter.value = "";
showSearch.value = false;
}
function isSearching() {
return showSearch.value && searchFilter.value;
}
return {
filteredMessages,
searchFilter,
searchQueryFilter,
isValidQuery,
debouncedSearchFilter,
showSearch,
markSearch,
resetSearch,
isSearching,
};
+15 -11
View File
@@ -7,17 +7,21 @@ 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];
const storage = useProfileStorage("visibleKeys", new Map<string, Map<string[], boolean>>(), {
from(transformed: [string, [string[], boolean][]][]) {
return new Map(transformed.map(([key, value]) => [key, new Map(value)]));
},
to(value: Map<string, Map<string[], boolean>>) {
const outer = Array.from(value.entries());
const inner = outer.map(([key, value]) => [key, Array.from(value.entries())]);
return inner;
},
});
export function persistentVisibleKeysForContainer(container: Ref<Container>): Ref<Map<string[], boolean>> {
// Computed property to only store to storage when the value changes
return computed({
get: () => storage.value.get(container.value.storageKey) || new Map<string[], boolean>(),
set: (value: Map<string[], boolean>) => storage.value.set(container.value.storageKey, value),
});
}
+17 -9
View File
@@ -1,16 +1,24 @@
import { ComplexLogEntry, type JSONObject, type LogEntry } from "@/models/LogEntry";
import type { Ref } from "vue";
export function useVisibleFilter(visibleKeys: Ref<string[][]>) {
export function useVisibleFilter(visibleKeys: Ref<Map<string[], boolean>>) {
const { isSearching } = useSearchFilter();
function filteredPayload(messages: Ref<LogEntry<string | JSONObject>[]>) {
return computed(() => {
return messages.value.map((d) => {
if (d instanceof ComplexLogEntry) {
return ComplexLogEntry.fromLogEvent(d, visibleKeys);
} else {
return d;
}
});
return messages.value
.map((d) => {
if (d instanceof ComplexLogEntry) {
return ComplexLogEntry.fromLogEvent(d, visibleKeys);
} else {
return d;
}
})
.filter((d) => {
if (isSearching.value && d instanceof ComplexLogEntry) {
return Object.values(d.message).some((v) => JSON.stringify(v)?.includes("<mark>"));
} else {
return true;
}
});
});
}
-5
View File
@@ -1,5 +0,0 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="128" height="128" rx="6" fill="#222222"/>
<path d="M82.3248 94.3863H123V104.093H67.8025V95.3506L106.164 44.3736H68.3808V34.5382H121.072V42.9594L82.3248 94.3863Z" fill="#FFDD57"/>
<path d="M8 107.107L17.5656 14L43.8372 16.7013C51.9339 17.5338 58.9091 20.0604 64.7629 24.2812C70.6166 28.5019 74.8873 34.0893 77.5749 41.0432C80.3052 48.0016 81.2514 55.7674 80.4137 64.3407L79.8027 70.2877C78.9005 79.0698 76.4053 86.5894 72.3173 92.8468C68.2719 99.1084 62.914 103.684 56.2436 106.574C49.6158 109.468 42.1213 110.529 33.7602 109.755L8 107.107ZM28.8005 25.3655L21.3043 98.3288L34.2164 99.6565C43.6767 100.629 51.3299 98.4435 57.1758 93.0993C63.0644 87.7595 66.5671 79.6542 67.684 68.7832L68.2424 63.3477C69.3286 52.7752 67.6788 44.3123 63.293 37.9592C58.9542 31.5678 52.2295 27.8607 43.1188 26.8377L28.8005 25.3655Z" fill="#FFDD57"/>
</svg>

Before

Width:  |  Height:  |  Size: 949 B

+15 -6
View File
@@ -11,14 +11,14 @@
<router-view></router-view>
</pane>
<template v-if="!isMobile">
<pane v-for="other in activeContainers" :key="other.id">
<log-container
<pane v-for="other in pinnedLogs" :key="other.id">
<ContainerLog
:id="other.id"
show-title
scrollable
closable
@close="containerStore.removeActiveContainer(other)"
></log-container>
@close="pinnedLogsStore.unPinContainer(other)"
/>
</pane>
</template>
</splitpanes>
@@ -42,6 +42,12 @@
<button>close</button>
</form>
</dialog>
<SideDrawer ref="drawer" :width="drawerWidth">
<Suspense :timeout="0">
<component :is="drawerComponent" v-bind="drawerProperties" />
<template #fallback> Loading dependencies... </template>
</Suspense>
</SideDrawer>
<div class="toast toast-end whitespace-normal">
<div
class="alert max-w-xl"
@@ -71,11 +77,14 @@
// @ts-ignore - splitpanes types are not available
import { Splitpanes, Pane } from "splitpanes";
import { collapseNav } from "@/stores/settings";
import SideDrawer from "@/components/common/SideDrawer.vue";
const containerStore = useContainerStore();
const { activeContainers } = storeToRefs(containerStore);
const pinnedLogsStore = usePinnedLogsStore();
const { pinnedLogs } = storeToRefs(pinnedLogsStore);
const { toasts, removeToast } = useToast();
const drawer = useTemplateRef<InstanceType<typeof SideDrawer>>("drawer") as Ref<InstanceType<typeof SideDrawer>>;
const { component: drawerComponent, properties: drawerProperties, width: drawerWidth } = createDrawer(drawer);
const modal = ref<HTMLDialogElement>();
const open = ref(false);
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 396 114"><path class="secondary-fill" d="M89.408 46.477a6.49 6.49 0 0 0-6.488 6.49 6.488 6.488 0 1 0 12.978 0 6.49 6.49 0 0 0-6.49-6.49m-42.836 0a6.49 6.49 0 1 0 0 12.98 6.49 6.49 0 0 0 0-12.98M28.5 38.295a6.5 6.5 0 0 1-1.897-4.593 6.474 6.474 0 0 1 6.49-6.49c1.663 0 3.327.631 4.592 1.896l-1.517 1.12a51.67 51.67 0 0 1 31.822-10.91c11.999 0 23.028 4.078 31.823 10.91L98.3 29.113a6.47 6.47 0 0 1 4.592-1.897c1.659 0 3.324.63 4.588 1.897a6.52 6.52 0 0 1 1.892 4.593 6.47 6.47 0 0 1-1.892 4.594l1.514 1.113c21.944 21.942-15.406 31.822-15.406 31.822H42.392c-1.077 0-38.88-8.35-15.406-31.822zM102.214 94.411a7.894 7.894 0 0 1-7.894-7.895 7.893 7.893 0 1 1 15.787 0 7.89 7.89 0 0 1-7.893 7.895M37.355 95.13a7.894 7.894 0 0 1-7.895-7.896 7.894 7.894 0 0 1 7.895-7.893 7.894 7.894 0 0 1 7.896 7.893 7.894 7.894 0 0 1-7.896 7.895M44.372 72.866h47.402s-22.981 7.899-47.402 0"/><path class="content-fill" d="M142 79.928V33.446h13.836c3.571 0 6.886.574 9.947 1.786 3.06 1.147 5.675 2.805 7.779 4.846 4.272 4.144 6.822 10.01 6.822 16.514 0 3.379-.637 6.567-1.913 9.5-1.275 2.933-3.06 5.356-5.228 7.396-4.4 4.017-10.648 6.44-17.407 6.44zm4.91-41.89v37.3h8.735c3.889 0 7.332-.83 10.265-2.423 2.997-1.595 5.356-3.762 6.95-6.568 1.658-2.805 2.487-6.057 2.487-9.628 0-5.292-1.977-10.01-5.356-13.39-3.315-3.315-8.48-5.292-14.346-5.292zm86.587 18.554c0 4.4-1.02 8.416-2.997 12.05q-3.06 5.452-8.416 8.608c-3.634 2.104-7.588 3.188-12.051 3.188-3.252 0-6.312-.637-9.245-1.849-2.869-1.211-5.356-2.869-7.397-5.037-2.04-2.04-3.634-4.527-4.845-7.46-1.148-2.933-1.786-5.993-1.786-9.245q0-6.6 3.061-12.051c1.976-3.635 4.782-6.568 8.416-8.672C201.808 34.02 205.825 33 210.224 33c3.252 0 6.313.574 9.246 1.785 2.869 1.276 5.356 2.933 7.396 4.974 2.04 2.168 3.698 4.654 4.846 7.523 1.211 2.934 1.785 5.994 1.785 9.31m-5.037.255c0-5.229-1.913-10.075-5.101-13.581s-7.97-5.739-13.326-5.739c-3.443 0-6.567.893-9.373 2.55-2.805 1.786-4.973 4.081-6.503 6.95q-2.296 4.4-2.296 9.565c0 5.164 1.913 10.074 5.101 13.517 3.188 3.507 7.97 5.739 13.262 5.739 3.444 0 6.568-.893 9.373-2.615 2.87-1.657 5.037-4.017 6.568-6.886 1.53-2.87 2.295-6.057 2.295-9.5m43.932 18.618v4.463h-33.539v-4.463l27.672-37.555H239.81v-4.464h32.454v4.464l-27.8 37.555zm38.192 0v4.463h-33.538v-4.463l27.672-37.555h-26.715v-4.464h32.454v4.464l-27.8 37.555zm8.927 4.463V33.446h4.91v41.891h22.061v4.591zm34.622 0V33.446h29.075v4.527h-24.165v16.068h22.125v4.527h-22.125v16.833h24.42v4.527z"/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

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