Compare commits

...

482 Commits

Author SHA1 Message Date
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
Amir Raminfar 011f55c55b chore: release v6.5.2 2024-04-26 14:37:32 -07:00
Amir Raminfar d7079c36e4 chore: logs swarm mode to beacon (#2918) 2024-04-26 21:30:32 +00:00
Amir Raminfar 7fb4dcce77 docs: updates homepage sub heading 2024-04-25 08:54:16 -07:00
Amir Raminfar 0699680c2f docs: updates homepage 2024-04-25 08:46:38 -07:00
renovate[bot] e91d643968 chore(deps): update dependency vitest to ^1.5.2 (#2917)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-25 15:35:28 +00:00
renovate[bot] d0b522116d chore(deps): update all non-major dependencies (#2916)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-25 01:16:29 +00:00
renovate[bot] 56ec36e10c chore(deps): update all non-major dependencies (#2914)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-23 17:53:48 +00:00
Amir Raminfar 588cd604ce feat: Add Turkish language support (#2912)
Co-authored-by: hasanbeder <hasanbeder@yahoo.com>
2024-04-22 08:03:01 -07:00
renovate[bot] c49b761f1f fix(deps): update all non-major dependencies (#2911)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-22 14:56:45 +00:00
renovate[bot] 22d8e86061 chore(deps): update pnpm to v9.0.5 (#2910)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-22 00:34:26 +00:00
renovate[bot] a6ca9635d6 chore(deps): update all non-major dependencies (#2908)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-20 20:34:33 +00:00
renovate[bot] b5b8650ce6 fix(deps): update module github.com/docker/docker to v26.0.2+incompatible [security] (#2906)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-19 00:51:07 +00:00
renovate[bot] 4d75978133 chore(deps): update all non-major dependencies (#2905)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-18 15:02:46 +00:00
renovate[bot] 534f21fa06 chore(deps): update all non-major dependencies (#2903)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-17 20:21:18 +00:00
Amir Raminfar 2f160a9d8b chore: release v6.5.1 2024-04-16 12:56:14 -07:00
Amir Raminfar f4dd11d136 feat: only groups containers when there is more than one (#2902) 2024-04-16 19:55:59 +00:00
Amir Raminfar 81705c8a47 feat: implement sticky collapsed and expanded groups on side menu (#2901) 2024-04-16 08:44:26 -07:00
renovate[bot] 2882961288 chore(deps): update all non-major dependencies (#2899)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 15:17:17 +00:00
renovate[bot] bac8737161 chore(deps): update pnpm to v9 (#2900) 2024-04-16 08:12:36 -07:00
renovate[bot] 4d9cc362df chore(deps): update all non-major dependencies (#2898)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 01:33:22 +00:00
Amir Raminfar 885db043ad chore: release v6.5.0 2024-04-14 14:22:48 -07:00
renovate[bot] f804e475d1 chore(deps): update all non-major dependencies (#2896)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-14 21:19:46 +00:00
Amir Raminfar e4e6903ef3 fix: removes toSorted() for older browsers that do not support it (#2895) 2024-04-14 14:06:11 -07:00
Amir Raminfar ec03e93298 feat: groups containers by stack or compose when possible (#2893) 2024-04-14 14:05:27 -07:00
renovate[bot] 56aa440c92 chore(deps): update all non-major dependencies (#2892)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-12 16:49:19 +00:00
Amir Raminfar 27989fca52 chore: release v6.4.6 2024-04-11 09:37:26 -07:00
Amir Raminfar 02c834359b fix: uses noindex instead of robots.txt (#2890) 2024-04-11 16:37:12 +00:00
Amir Raminfar 857896e448 chore: release v6.4.5 2024-04-11 09:26:29 -07:00
Amir Raminfar b781570811 fix: removes any configuration from index.html when users are not authenticated (#2889) 2024-04-11 09:03:37 -07:00
renovate[bot] ac04e4c149 chore(deps): update all non-major dependencies (#2888)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-11 15:27:50 +00:00
renovate[bot] 89d761d389 chore(deps): update all non-major dependencies (#2886)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-10 15:17:17 +00:00
Amir Raminfar fdb0984e3a docs: updates analytics docs 2024-04-10 08:09:24 -07:00
Amir Raminfar 9e99e043cd chore: release v6.4.4 2024-04-09 14:00:14 -07:00
Amir Raminfar 9aaae8ee91 fix: fixes actions with new routes. fixes #2883 (#2884) 2024-04-09 20:59:38 +00:00
Amir Raminfar 5110456537 chore: updates packages (#2882) 2024-04-09 10:59:46 -07:00
Amir Raminfar 6fdd5b788f docs: adds filter documentation 2024-04-09 09:18:25 -07:00
Amir Raminfar 2b6fb8556f chore: release v6.4.3 2024-04-08 14:03:18 -07:00
Amir Raminfar af7ed8b424 chore: fixes interval healthcheck (#2881) 2024-04-08 14:02:59 -07:00
renovate[bot] c63dc9dc13 chore(deps): update docker/setup-buildx-action action to v3.3.0 (#2880)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-08 19:04:00 +00:00
Amir Raminfar 83f488ecef fix: tries to reconnect of remote host disconnects (#2876) 2024-04-08 11:59:03 -07:00
renovate[bot] 3d2036c97a chore(deps): update all non-major dependencies (#2878)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-08 00:23:36 +00:00
renovate[bot] f3d49607a3 chore(deps): update all non-major dependencies (#2875)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-05 18:06:51 +00:00
Amir Raminfar 9b641a99da fix: fixes background color with ansi colors. fixes #2873 (#2874) 2024-04-05 10:56:05 -07:00
renovate[bot] d09e179445 fix(deps): update all non-major dependencies (#2872)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-04 15:48:53 +00:00
renovate[bot] daffeb17f8 chore(deps): update all non-major dependencies (#2871)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-03 20:30:30 +00:00
Amir Raminfar 3c7df537ec chore: changes APIs to be more restful and prepare for services (#2869) 2024-04-02 10:01:02 -07:00
renovate[bot] 376318bbbf chore(deps): update all non-major dependencies (#2868)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-02 16:26:38 +00:00
renovate[bot] c448636641 chore(deps): update actions/configure-pages action to v5 (#2864)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-30 07:39:13 -07:00
renovate[bot] 30323bf3af chore(deps): update dependency @types/node to ^20.12.2 (#2863)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-30 14:35:02 +00:00
renovate[bot] 296ed7945b chore(deps): update all non-major dependencies (#2860)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-29 15:00:33 +00:00
Amir Raminfar f343e12979 chore: enables static checker (#2859) 2024-03-28 10:35:10 -07:00
renovate[bot] b3b01d96a8 chore(deps): update all non-major dependencies (#2858)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-28 15:24:17 +00:00
renovate[bot] 783b7f0307 fix(deps): update all non-major dependencies (#2857)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-27 18:46:25 +00:00
Amir Raminfar cc26b1be81 chore: release v6.4.2 2024-03-26 09:07:37 -07:00
renovate[bot] 263ad3f235 chore(deps): update all non-major dependencies (#2855)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-26 15:50:54 +00:00
Amir Raminfar 9c1b967502 feat: improves icons placement for expanding json (#2854) 2024-03-26 15:46:05 +00:00
renovate[bot] 21c775d40b fix(deps): update dependency @intlify/unplugin-vue-i18n to v4 (#2853)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-26 08:43:20 -07:00
renovate[bot] 943d0a87a1 chore(deps): update all non-major dependencies (#2852)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-26 14:35:49 +00:00
Amir Raminfar a08e85cf7b chore: cleans up tests (#2850) 2024-03-25 15:33:16 -07:00
Amir Raminfar 3a2546f919 feat: hides per page dropdown when total number of containers is less than a page (#2849) 2024-03-25 08:09:10 -07:00
renovate[bot] d55e5f1c53 fix(deps): update dependency unplugin-vue-macros to ^2.7.13 (#2848)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-25 03:50:06 +00:00
Amir Raminfar 0639320dca chore: updates total test for collector (#2847) 2024-03-25 01:45:09 +00:00
renovate[bot] 293f6a075b fix(deps): update all non-major dependencies (#2846)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-25 01:32:55 +00:00
Amir Raminfar ef0938d6b8 chore: adds tests (#2845) 2024-03-23 21:32:17 -07:00
renovate[bot] 192d3783af fix(deps): update all non-major dependencies (#2843)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-22 19:11:29 +00:00
renovate[bot] 2d7c806390 fix(deps): update module github.com/docker/docker to v26 (#2841)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-21 09:16:51 -07:00
renovate[bot] e643099f34 fix(deps): update all non-major dependencies (#2840)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-21 16:14:48 +00:00
Amir Raminfar 821c3e90f2 chore: release v6.4.1 2024-03-21 09:09:25 -07:00
Amir Raminfar b72ee29aa3 fix: this fixes a critical bug where multiple stat collectors were competing and never stopping. see #2838 (#2839) 2024-03-21 16:09:01 +00:00
Amir Raminfar 83ef59cd88 chore: release v6.4.0 2024-03-20 11:37:27 -07:00
Amir Raminfar 0ca6624238 feat: deactivates collection of stats after 6 hours of idle (#2830) 2024-03-20 10:53:52 -07:00
renovate[bot] f9aabf138c fix(deps): update all non-major dependencies (#2837)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-20 16:12:39 +00:00
renovate[bot] 7ed2c50ba7 chore(deps): update all non-major dependencies (#2836)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 22:14:23 +00:00
Amir Raminfar 993b3fd2f6 fix: fixes parsing of logfmt logs with a regex. fixes #2834 (#2835) 2024-03-19 14:36:39 -07:00
Amir Raminfar 9bf6cd8821 chore: code fix 2024-03-19 14:27:57 -07:00
renovate[bot] cb99bcdc09 fix(deps): update dependency date-fns to ^3.6.0 (#2832)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-18 03:52:02 +00:00
renovate[bot] a8e4267c67 chore(deps): update all non-major dependencies (#2831)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-18 00:57:39 +00:00
renovate[bot] c0a80c5591 chore(deps): update all non-major dependencies (#2827)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-15 16:42:44 +00:00
Amir Raminfar 7f38bdaf4b chore: code clean up 2024-03-14 12:22:32 -07:00
Amir Raminfar c2fce52e1c perf: adds caching and brotli (#2826) 2024-03-14 12:11:36 -07:00
Amir Raminfar 09a9886423 perf: makes messages for copy and paste lazy for minor perf improvement 2024-03-14 08:29:16 -07:00
Amir Raminfar e7cabc8857 chore: release v6.3.1 2024-03-13 08:37:55 -07:00
Amir Raminfar 7e3d501f0f fix: fixes global regex with g flag. fixes #2822 (#2825) 2024-03-13 15:36:55 +00:00
Amir Raminfar c2dbcd546c chore: release v6.3.0 2024-03-12 12:20:41 -07:00
Amir Raminfar ec2be9c0f2 feat: brings colors back and fixes #2639 (#2823) 2024-03-12 19:15:18 +00:00
Amir Raminfar 32ed13d7d7 fix: uses data attributes for auto theme and removes postcss-mixins (#2820) 2024-03-11 16:57:47 +00:00
renovate[bot] f5b4cfd55a fix(deps): update all non-major dependencies (#2819)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-11 16:44:58 +00:00
renovate[bot] 4c07a7210b fix(deps): update all non-major dependencies (#2818)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-11 01:05:59 +00:00
Fabeuss 34d8ef6913 feat: Adds french (#2815) 2024-03-10 06:32:41 -07:00
Amir Raminfar 7fbac3b118 chore: fix types 2024-03-08 11:41:46 -08:00
renovate[bot] 4e59cf3937 fix(deps): update dependency @intlify/unplugin-vue-i18n to v3 (#2813)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-07 15:01:40 -08:00
renovate[bot] e02378f93f chore(deps): update all non-major dependencies (#2812)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-07 22:04:10 +00:00
Nikolaos Pothitos 32b8b02607 docs: add missing base slash and remove non-existent nginx configuration (#2809) 2024-03-05 16:10:01 -08:00
renovate[bot] 721e8e9e36 chore(deps): update all non-major dependencies (#2808)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 22:11:40 +00:00
Amir Raminfar 8055512bc4 chore: release v6.2.7 2024-03-05 10:46:33 -08:00
Amir Raminfar 61f26b7bc3 fix: redirects login page to previous page after successful login. see #2806 (#2807) 2024-03-05 18:42:46 +00:00
renovate[bot] e6528a7fc1 fix(deps): update dependency postcss-mixins to v10 (#2805)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 10:37:46 -08:00
renovate[bot] 378de1a12b fix(deps): update all non-major dependencies (#2804)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-04 23:23:22 +00:00
renovate[bot] c803fe3119 chore(deps): update dependency vue-tsc to v2 (#2803)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-04 08:43:58 -08:00
renovate[bot] 0e1541b78d chore(deps): update all non-major dependencies (#2802)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-03 22:07:55 +00:00
Amir Raminfar 1d14c6c6e7 chore: release v6.2.6 2024-03-01 13:09:02 -08:00
Amir Raminfar b78b70b233 feat: adds hideMenu as a query parameter (#2796) 2024-02-29 08:11:02 -08:00
renovate[bot] f9465a2d66 chore(deps): update all non-major dependencies (#2797)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-28 18:30:49 +00:00
renovate[bot] a5da85d603 chore(deps): update all non-major dependencies to ^3.4.20 (#2795)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-26 03:40:30 +00:00
renovate[bot] 2140fe3d37 fix(deps): update module github.com/puzpuzpuz/xsync/v3 to v3.1.0 (#2794)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-25 21:24:11 +00:00
renovate[bot] ff2e766324 chore(deps): update all non-major dependencies (#2791)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-24 16:21:17 +00:00
Amir Raminfar 336e8acd0b fix: improves fuzzy by using key down instead of up (#2790) 2024-02-23 21:09:14 +00:00
renovate[bot] a6c2c7dbbc fix(deps): update all non-major dependencies (#2789)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-21 17:38:43 +00:00
Amir Raminfar 8d844b4302 chore: release v6.2.5 2024-02-20 09:15:38 -08:00
Amir Raminfar ac66969126 chore: fixes types 2024-02-20 09:15:17 -08:00
renovate[bot] b896e16941 fix(deps): update all non-major dependencies (#2788)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-20 16:57:21 +00:00
Amir Raminfar 5b1b2463b0 fix: fixes missing nano seconds for parsing between logs. see #2783 (#2785) 2024-02-20 08:50:00 -08:00
Amir Raminfar 07d0c7fac5 fix: fixes changing items per page. fixes #2784 (#2786) 2024-02-19 23:12:16 +00:00
renovate[bot] d2d573c399 fix(deps): update all non-major dependencies (#2782)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-19 01:57:52 +00:00
renovate[bot] 54ad9d094b chore(deps): update all non-major dependencies (#2781)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-16 21:51:56 +00:00
renovate[bot] afb9bc971c fix(deps): update all non-major dependencies (#2778)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-14 16:42:38 +00:00
renovate[bot] 3e0d010390 chore(deps): update all non-major dependencies (#2777)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-13 16:39:46 +00:00
renovate[bot] 76e5b960aa fix(deps): update all non-major dependencies (#2776)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-12 20:16:36 +00:00
Amir Raminfar 152bdb8c8f feat: adds italian locale (#2775) 2024-02-12 15:53:06 +00:00
Amir Raminfar 77e49d132f chore: removes unused packages (#2772) 2024-02-11 13:43:55 -08:00
Amir Raminfar 3fe087935c chore: release v6.2.4 2024-02-10 09:46:49 -08:00
renovate[bot] b47cf7940a chore(deps): update all non-major dependencies (#2771)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-10 17:16:41 +00:00
Amir Raminfar 420b398e50 chore: uses 1.22 for testing (#2770) 2024-02-10 17:13:19 +00:00
Ivan 91d79c99ea feat: add Content-Encoding for downloading logs (#2769) 2024-02-10 06:02:10 -08:00
Amir Raminfar 28c2cdc571 chore: release v6.2.3 2024-02-09 11:13:20 -08:00
Amir Raminfar d67f2da1ba fix: uses github.com/goccy/go-json for less CPU (#2766) 2024-02-09 11:11:20 -08:00
Ahmet a99672a5da chore: adjust supported env variables in readme (#2768) 2024-02-08 23:18:09 +00:00
renovate[bot] eefa472516 fix(deps): update all non-major dependencies (#2764)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-08 18:58:12 +00:00
Amir Raminfar 11cde3d5fc chore: fixes improvements with go1.22 2024-02-07 12:33:28 -08:00
Amir Raminfar 935d76e539 chore: release v6.2.2 2024-02-07 11:20:47 -08:00
Amir Raminfar 2497d62fe2 chore: fixes tests 2024-02-07 11:13:33 -08:00
Amir Raminfar 7103cc3c5c fix: fixes initital load not being ready yet (#2763) 2024-02-07 18:53:19 +00:00
Amir Raminfar cd877c910c fix: fixes a possible race condition (#2762) 2024-02-07 17:48:48 +00:00
renovate[bot] 946f59e603 fix(deps): update all non-major dependencies (#2758)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-07 16:29:05 +00:00
Amir Raminfar a03e5d10aa chore: disables static check (#2760) 2024-02-07 16:24:02 +00:00
renovate[bot] 183326db52 fix(deps): update all non-major dependencies (#2754)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-05 01:57:41 +00:00
270 changed files with 17502 additions and 9176 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: 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 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.
+28 -11
View File
@@ -31,12 +31,18 @@ jobs:
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: 1.21.x
go-version: 1.22.x
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@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.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@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
- name: Login to DockerHub
uses: docker/login-action@v3.0.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.1.0
uses: docker/build-push-action@v6.7.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@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.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@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
- name: Login to DockerHub
uses: docker/login-action@v3.0.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.1.0
uses: docker/build-push-action@v6.7.0
with:
context: .
push: true
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }}
+1 -3
View File
@@ -1,5 +1,3 @@
# Sample workflow for building and deploying a VitePress site to GitHub Pages
#
name: Deploy VitePress site to Pages
on:
@@ -36,7 +34,7 @@ jobs:
node-version: latest
cache: pnpm # or pnpm / yarn
- name: Setup Pages
uses: actions/configure-pages@v4
uses: actions/configure-pages@v5
- name: Install dependencies
run: pnpm install
- name: Build with VitePress
+14 -4
View File
@@ -54,14 +54,22 @@ jobs:
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: 1.21.x
go-version: 1.23.1
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
- name: Stactic checker
uses: dominikh/staticcheck-action@v1.3.0
uses: dominikh/staticcheck-action@v1.3.1
with:
install-go: false
int-test:
@@ -85,8 +93,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-alpine as node
FROM --platform=$BUILDPLATFORM node:22.8.0-alpine AS node
RUN corepack enable
@@ -22,9 +22,13 @@ COPY public ./public
# Build assets
RUN pnpm build
FROM --platform=$BUILDPLATFORM golang:1.21.6-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.23.1-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
+46 -26
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)
@@ -15,15 +15,17 @@ https://user-images.githubusercontent.com/260667/227634771-9ebbe381-16a8-465a-b2
- Search logs using regex 🔦
- Small memory footprint 🏎
- Split screen for viewing multiple logs
- Download logs easily
- Live stats with memory and CPU usage
- Multi-user authentication with support for proxy forward authorization 🚨
- Swarm mode support 🐳
- 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 +33,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 +93,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,19 +110,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` |
| `--filter` | `DOZZLE_FILTER` | `""` |
| `--username` | `DOZZLE_USERNAME` | `""` |
| `--password` | `DOZZLE_PASSWORD` | `""` |
| `--usernamefile` | `DOZZLE_USERNAME_FILE` | `""` |
| `--passwordfile` | `DOZZLE_PASSWORD_FILE` | `""` |
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
| `--remote-host` | `DOZZLE_REMOTE_HOST` | |
| Flag | Env Variable | Default |
| --------------------- | -------------------------- | -------------- |
| `--addr` | `DOZZLE_ADDR` | `:8080` |
| `--base` | `DOZZLE_BASE` | `/` |
| `--hostname` | `DOZZLE_HOSTNAME` | `""` |
| `--level` | `DOZZLE_LEVEL` | `info` |
| `--auth-provider` | `DOZZLE_AUTH_PROVIDER` | `none` |
| `--auth-header-user` | `DOZZLE_AUTH_HEADER_USER` | `Remote-User` |
| `--auth-header-email` | `DOZZLE_AUTH_HEADER_EMAIL` | `Remote-Email` |
| `--auth-header-name` | `DOZZLE_AUTH_HEADER_NAME` | `Remote-Name` |
| `--enable-actions` | `DOZZLE_ENABLE_ACTIONS` | false |
| `--filter` | `DOZZLE_FILTER` | `""` |
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
| `--mode` | `DOZZLE_MODE` | `server` |
| `--remote-host` | `DOZZLE_REMOTE_HOST` | |
| `--remote-agent` | `DOZZLE_REMOTE_AGENT` | |
## Support
@@ -123,10 +143,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.
+5 -9
View File
@@ -3,6 +3,7 @@
</template>
<script lang="ts" setup>
const mode = useColorMode();
watchEffect(() => {
if (smallerScrollbars.value) {
document.documentElement.classList.add("has-custom-scrollbars");
@@ -10,16 +11,11 @@ watchEffect(() => {
document.documentElement.classList.remove("has-custom-scrollbars");
}
switch (lightTheme.value) {
case "dark":
document.documentElement.setAttribute("data-theme", "dark");
break;
case "light":
document.documentElement.setAttribute("data-theme", "light");
break;
default:
document.documentElement.removeAttribute("data-theme");
let theme = lightTheme.value;
if (theme === "auto") {
theme = mode.value;
}
document.documentElement.setAttribute("data-theme", theme);
});
</script>
<style lang="postcss">
+62 -35
View File
@@ -27,7 +27,6 @@ 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']
@@ -51,6 +50,7 @@ declare global {
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 +58,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 +72,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']
@@ -101,11 +102,14 @@ declare global {
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const persistentVisibleKeys: typeof import('./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 provideDetails: typeof import('./composable/showDetails')['provideDetails']
const provideLocal: typeof import('@vueuse/core')['provideLocal']
const provideLogDetails: typeof import('./composable/showLogDetails')['provideLogDetails']
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 +126,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']
@@ -132,6 +137,7 @@ declare global {
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const showAllContainers: typeof import('./stores/settings')['showAllContainers']
const showLogDetails: typeof import('./composable/showLogDetails')['showLogDetails']
const showStd: typeof import('./stores/settings')['showStd']
const showTimestamp: typeof import('./stores/settings')['showTimestamp']
const size: typeof import('./stores/settings')['size']
@@ -188,8 +194,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']
@@ -201,6 +207,7 @@ declare global {
const useDebounce: typeof import('@vueuse/core')['useDebounce']
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
const useDetails: typeof import('./composable/showDetails')['useDetails']
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
@@ -229,7 +236,9 @@ 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 useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
@@ -239,16 +248,17 @@ 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 useLogDetails: typeof import('./composable/showLogDetails')['useLogDetails']
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 useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
@@ -265,6 +275,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']
@@ -285,9 +296,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']
@@ -295,11 +308,13 @@ 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 useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
@@ -363,13 +378,6 @@ 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']>
@@ -385,7 +393,6 @@ 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']>
@@ -409,6 +416,7 @@ declare module 'vue' {
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']>
@@ -416,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']>
@@ -429,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,11 +468,13 @@ declare module 'vue' {
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly persistentVisibleKeys: UnwrapRef<typeof import('./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 provideLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['provideLogDetails']>
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']>
@@ -479,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']>
@@ -489,6 +502,7 @@ declare module 'vue' {
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly showAllContainers: UnwrapRef<typeof import('./stores/settings')['showAllContainers']>
readonly showLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['showLogDetails']>
readonly showStd: UnwrapRef<typeof import('./stores/settings')['showStd']>
readonly showTimestamp: UnwrapRef<typeof import('./stores/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./stores/settings')['size']>
@@ -545,8 +559,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']>
@@ -586,7 +600,9 @@ 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 useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
@@ -596,16 +612,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 useLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['useLogDetails']>
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 useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
@@ -622,6 +638,7 @@ declare module 'vue' {
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']>
@@ -642,9 +659,11 @@ declare module 'vue' {
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']>
@@ -652,11 +671,13 @@ declare module 'vue' {
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 useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
@@ -713,13 +734,6 @@ declare module 'vue' {
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']>
@@ -735,7 +749,6 @@ declare module '@vue/runtime-core' {
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']>
@@ -759,6 +772,7 @@ declare module '@vue/runtime-core' {
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']>
@@ -766,6 +780,7 @@ declare module '@vue/runtime-core' {
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']>
@@ -779,6 +794,7 @@ declare module '@vue/runtime-core' {
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']>
@@ -808,11 +824,13 @@ declare module '@vue/runtime-core' {
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly persistentVisibleKeys: UnwrapRef<typeof import('./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 provideLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['provideLogDetails']>
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']>
@@ -829,6 +847,7 @@ declare module '@vue/runtime-core' {
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly 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']>
@@ -839,6 +858,7 @@ declare module '@vue/runtime-core' {
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly showAllContainers: UnwrapRef<typeof import('./stores/settings')['showAllContainers']>
readonly showLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['showLogDetails']>
readonly showStd: UnwrapRef<typeof import('./stores/settings')['showStd']>
readonly showTimestamp: UnwrapRef<typeof import('./stores/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./stores/settings')['size']>
@@ -895,8 +915,8 @@ declare module '@vue/runtime-core' {
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']>
@@ -936,7 +956,9 @@ declare module '@vue/runtime-core' {
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 useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
@@ -946,16 +968,16 @@ declare module '@vue/runtime-core' {
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 useLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['useLogDetails']>
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 useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
@@ -972,6 +994,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']>
@@ -992,9 +1015,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']>
@@ -1002,11 +1027,13 @@ 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 useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
+36 -14
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,33 +23,36 @@ 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']
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']
'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']
@@ -60,17 +61,32 @@ 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:hamburgerMenu': typeof import('~icons/mdi/hamburger-menu')['default']
'Mdi:hexagonMultiple': typeof import('~icons/mdi/hexagon-multiple')['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: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']
@@ -78,15 +94,21 @@ 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']
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>
+75 -28
View File
@@ -1,14 +1,51 @@
!
<template>
<div class="text-right">
Show per page
<dropdown-menu
class="dropdown-left btn-xs md:btn-sm"
v-model="perPage"
:options="pageSizes.map((i) => ({ label: i.toLocaleString(), value: i }))"
v-if="containers.length > 0"
/>
<div 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>
<!-- @vue-ignore -->
<dropdown-menu
class="btn-sm"
v-model="selectedHost"
:options="[
{ label: 'Show All', value: null },
...Object.values(hosts).map((host) => ({ label: host.name, value: host.id })),
]"
v-else
/>
</div>
<div class="flex-1 text-right" v-show="containers.length > pageSizes[0]">
{{ $t("label.per-page") }}
<!-- @vue-ignore -->
<dropdown-menu
class="dropdown-left btn-xs md:btn-sm"
v-model="perPage"
:options="pageSizes.map((i) => ({ label: i.toLocaleString(), value: i }))"
/>
</div>
</div>
<table class="table table-lg bg-base">
<thead>
<tr :data-direction="direction > 0 ? 'asc' : 'desc'">
@@ -31,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>
@@ -41,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>
@@ -71,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",
@@ -117,18 +163,19 @@ 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));
const isPaginated = computed(() => totalPages.value > 1);
const currentPage = ref(1);
watch(perPage, () => (currentPage.value = 1));
const paginated = computed(() => {
const start = (currentPage.value - 1) * perPage.value;
const end = start + perPage.value;
@@ -8,18 +8,16 @@
<li>
<a @click.prevent="clear()">
<octicon:trash-24 /> {{ $t("toolbar.clear") }}
<key-shortcut char="k" :modifiers="['shift', 'meta']"></key-shortcut>
<KeyShortcut char="k" :modifiers="['shift', 'meta']" />
</a>
</li>
<li>
<a :href="`${base}/api/logs/download/${container.host}/${container.id}`" download>
<octicon:download-24 /> {{ $t("toolbar.download") }}
</a>
<a :href="downloadUrl" download> <octicon:download-24 /> {{ $t("toolbar.download") }} </a>
</li>
<li>
<a @click.prevent="showSearch = true">
<mdi:magnify /> {{ $t("toolbar.search") }}
<key-shortcut char="f"></key-shortcut>
<KeyShortcut char="f" />
</a>
</li>
<li class="line"></li>
@@ -103,15 +101,30 @@
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
const { showSearch } = useSearchFilter();
const { base, enableActions } = config;
const { enableActions } = config;
const clear = defineEmit();
const { container, streamConfig } = useContainerContext();
const { streamConfig } = useLoggingContext();
// container context is provided in the parent component: <LogContainer>
const { actionStates, start, stop, restart } = useContainerActions();
const { container } = defineProps<{ container: Container }>();
const { actionStates, start, stop, restart } = useContainerActions(toRef(() => container));
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(() => {
return actionStates.stop || actionStates.start || actionStates.restart;
@@ -0,0 +1,51 @@
<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";
import { useTemplateRef } from "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(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);
}
},
});
+8 -7
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 () => {
@@ -73,7 +74,7 @@ describe("<FuzzySearchModal />", () => {
test("choose baz", async () => {
const wrapper = createFuzzySearchModal();
await wrapper.find("input").setValue("baz");
await wrapper.find("input").trigger("keyup.enter");
expect(useRouter().push).toHaveBeenCalledWith({ name: "container-id", params: { id: "567" } });
await wrapper.find("input").trigger("keydown.enter");
expect(useRouter().push).toHaveBeenCalledWith({ name: "/container/[id]", params: { id: "567" } });
});
});
+130 -70
View File
@@ -4,108 +4,157 @@
<mdi:magnify class="flex size-8" />
<input
tabindex="0"
class="input input-ghost input-lg flex-1 px-1"
class="input input-lg input-ghost flex-1 px-1"
ref="input"
@keyup.down="selectedIndex = Math.min(selectedIndex + 1, data.length - 1)"
@keyup.up="selectedIndex = Math.max(selectedIndex - 1, 0)"
@keyup.enter.exact="selected(data[selectedIndex].item)"
@keyup.alt.enter="addColumn(data[selectedIndex])"
@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].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
.toSorted((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;
@@ -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" />
</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"
+20 -32
View File
@@ -1,49 +1,47 @@
<template>
<div class="relative flex w-full gap-x-2">
<div
class="group/item relative flex w-full cursor-pointer gap-x-2 hover:bg-secondary/10"
@click="showLogDetails(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 }" @click="expandToggle()">
<li v-for="(value, name) in validValues" class="inline-block">
<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";
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 showLogDetails = useLogDetails();
</script>
<style lang="postcss" scoped>
@@ -52,18 +50,8 @@ const validValues = computed(() => {
}
.fields {
&:hover {
&::after {
content: "expand json";
@apply ml-2 inline-block text-secondary;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
}
&.expanded:hover {
&::after {
content: "collapse json";
}
li {
@apply inline-flex;
}
}
</style>
@@ -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",
)
.toSorted((a, b) => +a.created - +b.created)[0],
);
function redirectNow() {
showToast(
{
title: t("alert.redirected.title"),
message: t("alert.redirected.message", { containerId: nextContainer.value.id }),
type: "info",
},
{ expire: 5000 },
);
router.push({ name: "container-id", params: { id: nextContainer.value.id } });
}
</script>
<style lang="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,9 @@ 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";
vi.mock("@/stores/config", () => ({
__esModule: true,
@@ -21,7 +21,7 @@ vi.mock("@/stores/config", () => ({
/**
* @vitest-environment jsdom
*/
describe("<LogEventSource />", () => {
describe("<ContainerEventSource />", () => {
const search = useSearchFilter();
beforeEach(() => {
@@ -50,7 +50,7 @@ describe("<LogEventSource />", () => {
},
) {
settings.value.hourStyle = hourStyle;
search.searchFilter.value = searchFilter;
search.searchQueryFilter.value = searchFilter;
if (searchFilter) {
search.showSearch.value = true;
}
@@ -67,32 +67,38 @@ describe("<LogEventSource />", () => {
],
});
return mount(LogEventSource, {
return mount(Component<Container>, {
global: {
plugins: [router, createTestingPinia({ createSpy: vi.fn }), createI18n({})],
components: {
ContainerLogViewer,
LogViewer,
},
provide: {
[containerContext as symbol]: {
container: computed(() => ({ id: "abc", image: "test:v123", host: "localhost" })),
[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 }),
},
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/logs/stream/localhost/abc?stdout=1&stderr=1";
const sourceUrl = "/api/hosts/localhost/containers/abc/logs/stream?stdout=1&stderr=1";
test("renders correctly", async () => {
test("renders loading correctly", async () => {
const wrapper = createLogEventSource();
expect(wrapper.html()).toMatchSnapshot();
});
@@ -140,24 +146,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 +162,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()) {
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">
<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="toggleField(name)" class="link-primary cursor-pointer">
{{ hasField(name) ? "remove" : "add" }}&nbsp;</a
>
<span class="text-light">{{ name }}=</span>[
<span class="font-bold" v-for="(item, index) in value">
<span v-html="JSON.stringify(item)"></span><span v-if="index !== value.length - 1">,</span>
</span>
]
</template>
<template v-else>
<a @click="toggleField(name)" class="link-primary cursor-pointer">
{{ hasField(name) ? "remove" : "add" }}&nbsp;</a
>
<span class="text-light">{{ name }}=</span><span class="font-bold" v-html="value"></span>
</template>
</li>
</ul>
</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>
@@ -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<{
+189
View File
@@ -0,0 +1,189 @@
<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" @change="toggleAll($event)" /></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);
visibleKeys.value.set(key, !enabled);
}
function toggleAll(e: Event) {
if (visibleKeys.value.size === 0) {
visibleKeys.value = new Map<string[], boolean>(fields.value.map(({ key }) => [key, true]));
}
const enabled = e.target instanceof HTMLInputElement && e.target.checked;
for (const key of visibleKeys.value.keys()) {
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: false });
}
}
}
return fieldsWithValue;
},
set(value) {
const map = new Map<string[], boolean>();
for (const { key, enabled } of value) {
map.set(key, enabled);
}
visibleKeys.value = map;
},
});
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>
+102
View File
@@ -0,0 +1,102 @@
<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] 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>
@@ -2,37 +2,24 @@
<div class="flex gap-2">
<div
class="flex min-w-[0.98rem] items-start justify-end align-bottom hover:cursor-pointer"
v-if="isSupported && message.trim() != ''"
v-if="isSupported"
:title="t('log_actions.copy_log')"
>
<span
class="rounded bg-slate-800/60 px-1.5 py-1 text-primary hover:bg-slate-700"
@click="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<{
message: string;
const { message } = defineProps<{
message: () => string;
logEntry: LogEntry<string | JSONObject>;
}>();
@@ -40,11 +27,8 @@ const { showToast } = useToast();
const { copy, isSupported, copied } = useClipboard();
const { t } = useI18n();
const { isSearching } = useSearchFilter();
const { handleJumpLineSelected } = useLogSearchContext();
async function copyLogMessageToClipBoard() {
await copy(message);
await copy(message());
if (copied.value) {
showToast(
+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>
+44 -59
View File
@@ -1,67 +1,52 @@
<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>
<SideDrawer ref="drawer">
<LogDetails :entry="entry" v-if="entry && entry instanceof ComplexLogEntry" />
</SideDrawer>
<LogList :messages="visibleMessages" :show-container-name="showContainerName" />
</template>
<script lang="ts" setup>
import { toRaw } from "vue";
import SideDrawer from "@/components/common/SideDrawer.vue";
import { ComplexLogEntry, type JSONObject, LogEntry } from "@/models/LogEntry";
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 drawer = ref<InstanceType<typeof SideDrawer>>() as Ref<InstanceType<typeof SideDrawer>>;
const { entry } = provideLogDetails(drawer);
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>
@@ -0,0 +1,83 @@
<template>
<div class="dropdown dropdown-end dropdown-hover">
<label tabindex="0" class="btn btn-ghost btn-sm gap-0.5 px-2">
<carbon:circle-solid class="w-2.5 text-red" v-if="streamConfig.stderr" />
<carbon:circle-solid class="w-2.5 text-blue" v-if="streamConfig.stdout" />
</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 @click.prevent="showSearch = true">
<mdi:magnify /> {{ $t("toolbar.search") }}
<KeyShortcut char="f" />
</a>
</li>
<li class="line"></li>
<li>
<a
@click="
streamConfig.stdout = true;
streamConfig.stderr = true;
"
>
<div class="flex size-4 gap-0.5">
<template v-if="streamConfig.stderr && streamConfig.stdout">
<carbon:circle-solid class="w-2 text-red" />
<carbon:circle-solid class="w-2 text-blue" />
</template>
</div>
{{ $t("toolbar.show-all") }}
</a>
</li>
<li>
<a
@click="
streamConfig.stdout = true;
streamConfig.stderr = false;
"
>
<div class="flex size-4 flex-col gap-1">
<carbon:circle-solid class="w-2 text-blue" v-if="!streamConfig.stderr && streamConfig.stdout" />
</div>
{{ $t("toolbar.show", { std: "STDOUT" }) }}
</a>
</li>
<li>
<a
@click="
streamConfig.stdout = false;
streamConfig.stderr = true;
"
>
<div class="flex size-4 flex-col gap-1">
<carbon:circle-solid class="w-2 text-red" v-if="streamConfig.stderr && !streamConfig.stdout" />
</div>
{{ $t("toolbar.show", { std: "STDERR" }) }}
</a>
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
const { showSearch } = useSearchFilter();
const clear = defineEmit();
const { streamConfig } = useLoggingContext();
</script>
<style scoped lang="postcss">
li.line {
@apply h-px bg-base-content/20;
}
a {
@apply whitespace-nowrap;
}
</style>
@@ -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>
+27 -9
View File
@@ -1,15 +1,16 @@
<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="markSearch(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(logEntry.message)"
:message="() => decodeXML(stripAnsi(logEntry.message))"
:log-entry="logEntry"
/>
</div>
@@ -17,10 +18,27 @@
<script lang="ts" setup>
import { SimpleLogEntry } from "@/models/LogEntry";
import { decodeXML } from "entities";
import AnsiConvertor from "ansi-to-html";
import stripAnsi from "strip-ansi";
defineProps<{
const ansiConvertor = new AnsiConvertor({
escapeXML: false,
fg: "var(--base-content-color)",
bg: "var(--base-color)",
});
const { showContainerName = false } = defineProps<{
logEntry: SimpleLogEntry;
showContainerName?: boolean;
}>();
const { markSearch } = useSearchFilter();
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,30 @@
<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 LogEventSource from "@/components/ContainerViewer/LogEventSource.vue";
import { LogStreamSource } from "@/composable/eventStreams";
const { streamSource, visibleKeys, showContainerName, entity } = defineProps<{
streamSource: (t: Ref<T>) => LogStreamSource;
visibleKeys: Map<string[], boolean>;
showContainerName: boolean;
entity: T;
}>();
const source = $ref<InstanceType<typeof LogEventSource>>();
defineExpose({
clear: () => source?.clear(),
});
onKeyStroke("k", (e) => {
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
source?.clear();
e.preventDefault();
}
});
</script>
@@ -0,0 +1,85 @@
// 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="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="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="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>
<dialog data-v-43493931="" class="modal-right modal items-start outline-none backdrop:bg-none">
<div data-v-43493931="" class="modal-box">
<form data-v-43493931="" method="dialog"><button data-v-43493931="" class="swap swap-rotate absolute right-4 top-4 outline-none hover:swap-active"><svg data-v-43493931="" viewBox="0 0 24 24" width="1.2em" height="1.2em" class="swap-off">
<path fill="currentColor" d="M1 7h6v2H3v2h4v2H3v2h4v2H1zm10 0h4v2h-4v2h2a2 2 0 0 1 2 2v2c0 1.11-.89 2-2 2H9v-2h4v-2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2m8 0h2a2 2 0 0 1 2 2v1h-2V9h-2v6h2v-1h2v1c0 1.11-.89 2-2 2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2"></path>
</svg><svg data-v-43493931="" viewBox="0 0 24 24" width="1.2em" height="1.2em" class="swap-on">
<path fill="currentColor" d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z"></path>
</svg></button></form>
</div>
<form data-v-43493931="" method="dialog" class="modal-backdrop"><button data-v-43493931="">close</button></form>
</dialog>
<!--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>
+9 -9
View File
@@ -16,22 +16,22 @@
<script lang="ts" setup>
import { globalShowPopup } from "@/composable/popup";
let glopbalShow = globalShowPopup();
let show = ref(glopbalShow.value);
let delayedShow = refDebounced(show, 1000);
let content: HTMLElement | null = $ref(null);
const glopbalShow = globalShowPopup();
const show = ref(glopbalShow.value);
const delayedShow = refDebounced(show, 1000);
const content = ref<HTMLElement>();
const onMouseEnter = (e: Event) => {
show.value = true;
glopbalShow.value = true;
if (e.target && content && e.target instanceof Element) {
if (content.value && e.target instanceof HTMLElement) {
const { left, top, width } = e.target.getBoundingClientRect();
const x = left + width + 10;
const y = top;
content.style.left = `${x}px`;
content.style.top = `${y}px`;
content.value.style.left = `${x}px`;
content.value.style.top = `${y}px`;
}
};
@@ -40,7 +40,7 @@ const onMouseLeave = () => {
glopbalShow.value = false;
};
const el = useCurrentElement();
const el: Ref<HTMLElement> = useCurrentElement();
useEventListener(() => el.value?.nextElementSibling, "mouseenter", onMouseEnter);
useEventListener(() => el.value?.nextElementSibling, "mouseleave", onMouseLeave);
</script>
+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;
+56 -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 bg-gradient-radial from-primary to-transparent to-70%"
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,21 @@ function setLoading(value: boolean) {
.fade-leave-to {
@apply opacity-0;
}
.animate-background {
animation: gradient-animation 2s ease infinite;
background-position: center;
}
@keyframes gradient-animation {
0%,
100% {
background-size: 100% 100%;
}
50% {
background-size: 140% 120%;
}
}
</style>
<style>
+6 -3
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,7 +29,7 @@
<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);
@@ -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>
+24 -189
View File
@@ -1,59 +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>
<transition :name="sessionHost ? 'slide-left' : 'slide-right'" mode="out-in">
<ul class="menu p-0" v-if="!sessionHost">
<li v-for="host in config.hosts">
<a @click.prevent="setHost(host.id)">
<ph:computer-tower />
{{ host.name }}
</a>
</li>
</ul>
<transition-group tag="ul" name="list" class="containers menu p-0 [&_li.menu-title]:px-0" v-else>
<li
v-for="item in menuItems"
:key="isContainer(item) ? item.id : item.keyLabel"
:class="isContainer(item) ? item.state : 'menu-title'"
:data-testid="isContainer(item) ? null : item.keyLabel"
>
<popup v-if="isContainer(item)">
<router-link
:to="{ name: 'container-id', params: { id: item.id } }"
active-class="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>
<template v-else>
{{ $t(item.keyLabel) }}
</template>
</li>
</transition-group>
</transition>
<Toggle v-model="showSwarm" v-if="services.length > 0 || customGroups.length > 0">
<div class="text-lg font-light">{{ $t("label.swarm-mode") }}</div>
</Toggle>
<SlideTransition :slide-right="showSwarm">
<template #left>
<HostMenu />
</template>
<template #right>
<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>
@@ -62,144 +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);
const store = useContainerStore();
const swarmStore = useSwarmStore();
const { services, customGroups } = storeToRefs(swarmStore);
const { activeContainers, visibleContainers, ready } = storeToRefs(store);
const showSwarm = useSessionStorage<boolean>("DOZZLE_SWARM_MODE", false);
function setHost(host: string | null) {
sessionHost.value = host;
}
const debouncedIds = debouncedRef(pinnedContainers, 200);
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 groupedContainers = computed(() =>
sortedContainers.value.reduce(
(acc, item) => {
if (debouncedIds.value.has(item.name)) {
acc.pinned.push(item);
} else {
acc.unpinned.push(item);
}
return acc;
},
{ pinned: [] as Container[], unpinned: [] as Container[] },
),
);
function isContainer(item: any): item is Container {
return item.hasOwnProperty("image");
}
const menuItems = computed(() => {
const pinnedLabel = { keyLabel: "label.pinned" };
const allLabel = { keyLabel: showAllContainers.value ? "label.all-containers" : "label.running-containers" };
if (groupedContainers.value.pinned.length > 0) {
return [pinnedLabel, ...groupedContainers.value.pinned, allLabel, ...groupedContainers.value.unpinned];
} else {
return [allLabel, ...groupedContainers.value.unpinned];
onBeforeRouteLeave((to) => {
if (to.meta.swarmMode) {
showSwarm.value = true;
} else if (to.meta.containerMode) {
showSwarm.value = false;
}
});
const hosts = computed(() =>
config.hosts.reduce(
(acc, item) => {
acc[item.id] = item;
return acc;
},
{} as Record<string, { name: string; id: string }>,
),
);
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>
+6 -5
View File
@@ -1,7 +1,7 @@
<template>
<aside class="fixed h-screen w-[inherit] overflow-auto p-3" data-testid="navigation">
<h1>
<router-link :to="{ name: 'index' }">
<router-link :to="{ name: '/' }">
<svg class="h-14 w-28 fill-secondary">
<use href="#logo"></use>
</svg>
@@ -12,15 +12,16 @@
</small>
</h1>
<a
<button
class="input input-sm mt-4 inline-flex cursor-pointer items-center gap-2 font-light hover:border-primary"
@click="$emit('search')"
:title="$t('tooltip.search')"
data-testid="search"
>
<mdi:magnify />
Search
<key-shortcut char="k"></key-shortcut>
</a>
{{ $t("placeholder.search") }}
<key-shortcut char="k" class="text-base-content/70"></key-shortcut>
</button>
<side-menu class="mt-4"></side-menu>
</aside>
@@ -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
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>
+2 -44
View File
@@ -1,7 +1,7 @@
<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' }">
<router-link :to="{ name: '/' }">
<svg class="h-14 w-28 fill-secondary">
<use href="#logo"></use>
</svg>
@@ -21,61 +21,19 @@
<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();
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 {
+34
View File
@@ -0,0 +1,34 @@
<template>
<dialog ref="panel" class="modal-right modal items-start outline-none backdrop:bg-none">
<div class="modal-box">
<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></slot>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
</template>
<script setup lang="ts">
const panel = ref<HTMLDialogElement>();
defineExpose({
open: () => {
panel.value?.showModal();
},
});
</script>
<style scoped lang="postcss">
.modal-right :where(.modal-box) {
@apply fixed right-0 h-lvh max-h-screen max-w-3xl translate-x-24 scale-100 rounded-none bg-base-lighter shadow-none;
}
.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="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",
});
+2 -2
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" />
</template>
</labeled-input>
</LabeledInput>
</template>
<script lang="ts" setup>
+4 -3
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({
@@ -10,7 +11,7 @@ export const useContainerActions = () => {
});
async function actionHandler(action: ContainerActions) {
const actionUrl = `/api/actions/${action}/${container.value.host}/${container.value.id}`;
const actionUrl = `/api/hosts/${container.value.host}/containers/${container.value.id}/actions/${action}`;
const errors = {
404: "container not found",
-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;
};
+199
View File
@@ -0,0 +1,199 @@
import { ShallowRef, type Ref } from "vue";
import debounce from "lodash.debounce";
import {
type LogEvent,
type JSONObject,
LogEntry,
asLogEntry,
ContainerEventLogEntry,
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 } = useLoggingContext();
const params = computed(() => {
const params = Object.entries(toValue(streamConfig))
.filter(([, value]) => value)
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), {} as Record<string, string>);
if (isSearching.value) {
params["filter"] = debouncedSearchFilter.value;
}
return params;
});
const urlWithParams = computed(() => withBase(`${url.value}?${new URLSearchParams(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 });
let fetchingInProgress = false;
async function loadOlderLogs() {
if (!loadMoreUrl) return;
if (fetchingInProgress) 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;
fetchingInProgress = true;
try {
const moreParams = { ...params.value, from: from.toISOString(), to: to.toISOString(), minimum: "100" };
const urlWithMoreParams = computed(() =>
withBase(`${loadMoreUrl.value}?${new URLSearchParams(moreParams).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 {
fetchingInProgress = false;
}
}
onScopeDispose(() => close());
const isLoadingMore = () => fetchingInProgress;
return { messages, loadOlderLogs, isLoadingMore };
}
-156
View File
@@ -1,156 +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/logs/stream/${container.value.host}/${containerId}?${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/logs/${container.value.host}/${containerId}?${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 };
}
+37
View File
@@ -0,0 +1,37 @@
import { Container } from "@/models/Container";
type LogContext = {
streamConfig: { stdout: boolean; stderr: boolean };
containers: Container[];
loadingMore: boolean;
};
// 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,
}),
);
};
export const useLoggingContext = () => {
const context = inject(
loggingContextKey,
reactive({
streamConfig: { stdout: true, stderr: true },
containers: [],
loadingMore: false,
}),
);
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 ? "i" : undefined);
});
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,
};
+22
View File
@@ -0,0 +1,22 @@
import { JSONObject, LogEntry } from "@/models/LogEntry";
import SideDrawer from "@/components/common/SideDrawer.vue";
export const showLogDetails = Symbol("showLogDetails") as InjectionKey<
(logEntry: LogEntry<string | JSONObject>) => void
>;
export const provideLogDetails = (drawer: Ref<InstanceType<typeof SideDrawer>>) => {
const entry = ref<LogEntry<string | JSONObject>>();
provide(showLogDetails, (logEntry: LogEntry<string | JSONObject>) => {
entry.value = logEntry;
drawer.value?.open();
});
return { entry };
};
export const useLogDetails = () => {
const showDetails = inject(showLogDetails, () => {});
return showDetails;
};
+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;
}
});
});
}
+11 -9
View File
@@ -1,8 +1,8 @@
<template>
<div>
<mobile-menu v-if="isMobile" @search="showFuzzySearch"></mobile-menu>
<mobile-menu v-if="isMobile && !forceMenuHidden" @search="showFuzzySearch"></mobile-menu>
<splitpanes @resized="onResized($event)">
<pane min-size="10" :size="menuWidth" v-if="!isMobile && !collapseNav">
<pane min-size="10" :size="menuWidth" v-if="!isMobile && !collapseNav && !forceMenuHidden">
<side-panel @search="showFuzzySearch"></side-panel>
</pane>
<pane min-size="10">
@@ -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>
@@ -27,7 +27,7 @@
<label
class="btn btn-circle swap btn-neutral swap-rotate fixed -left-12 bottom-4 w-16 transition-all hover:-left-4"
:class="{ '!-left-6': collapseNav }"
v-if="!isMobile"
v-if="!isMobile && !forceMenuHidden"
>
<input type="checkbox" v-model="collapseNav" />
<mdi:chevron-right class="swap-on" />
@@ -72,13 +72,15 @@
import { Splitpanes, Pane } from "splitpanes";
import { collapseNav } from "@/stores/settings";
const containerStore = useContainerStore();
const { activeContainers } = storeToRefs(containerStore);
const pinnedLogsStore = usePinnedLogsStore();
const { pinnedLogs } = storeToRefs(pinnedLogsStore);
const { toasts, removeToast } = useToast();
const modal = ref<HTMLDialogElement>();
const open = ref(false);
const searchParams = new URLSearchParams(window.location.search);
const forceMenuHidden = ref(searchParams.has("hideMenu"));
watch(open, () => {
if (open.value) {
+15 -30
View File
@@ -1,23 +1,5 @@
@import "splitpanes/dist/splitpanes.css";
@define-mixin light {
--base-lighter-color: 100% 0 0;
--base-color: 97% 0 0;
--base-darker-color: 90% 0 0;
--base-content-color: 33% 0 0;
--primary-color: 79.96% 0.143 176.65;
--secondary-color: 82.67% 0.15380739746702807 77.82;
}
@define-mixin dark {
--base-lighter-color: 25% 0 0;
--base-color: 18% 0 0;
--base-darker-color: 11% 0 0;
--base-content-color: 89.23% 0 0;
--primary-color: 70.96% 0.143 176.65;
--secondary-color: 91.36% 0.11098 90.1243;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
@@ -35,27 +17,30 @@
--in: 65% 0.171 249.5;
--inc: 100% 0 0;
--er: 64% 0.218 28.85;
--su: 56% 0.119722 164.12;
--erc: 100% 0 0;
--wa: 70% 0.186 48.13;
--wac: 100% 0 0;
--n: var(--base-lighter-color);
}
html[data-theme="dark"] {
@mixin dark;
}
@media (prefers-color-scheme: dark) {
html {
@mixin dark;
}
--base-lighter-color: 25% 0 0;
--base-color: 18% 0 0;
--base-darker-color: 11% 0 0;
--base-content-color: 89.23% 0 0;
--primary-color: 70.96% 0.143 176.65;
--secondary-color: 91.36% 0.11098 90.1243;
}
html[data-theme="light"] {
@mixin light;
}
@media (prefers-color-scheme: light) {
html {
@mixin light;
}
--base-lighter-color: 100% 0 0;
--base-color: 97% 0 0;
--base-darker-color: 90% 0 0;
--base-content-color: 33% 0 0;
--primary-color: 79.96% 0.143 176.65;
--secondary-color: 82.67% 0.15380739746702807 77.82;
}
@media screen and (max-device-width: 480px) {
body {
-webkit-text-size-adjust: 100%;
+2 -2
View File
@@ -3,8 +3,8 @@ import { createApp, App as VueApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
Object.values(import.meta.glob<{ install: (app: VueApp) => void }>("./modules/*.ts", { eager: true })).forEach(
(i) => i.install?.(app),
Object.values(import.meta.glob<{ install: (app: VueApp) => void }>("./modules/*.ts", { eager: true })).forEach((i) =>
i.install?.(app),
);
app.mount("#app");
-25
View File
@@ -1,25 +0,0 @@
import { describe, expect, test, vi } from "vitest";
import { Container } from "./Container";
vi.mock("@/stores/config", () => ({
__esModule: true,
default: { base: "", hosts: [{ name: "localhost", id: "localhost" }] },
}));
describe("Container", () => {
const names = [
[
"foo.gb1cto7gaq68fp4refnsr5hep.byqr1prci82zyfoos6gx1yhz0",
"foo",
".gb1cto7gaq68fp4refnsr5hep.byqr1prci82zyfoos6gx1yhz0",
],
["bar.gb1cto7gaq68fp4refnsr5hep", "bar", ".gb1cto7gaq68fp4refnsr5hep"],
["baz", "baz", null],
];
test.each(names)("name %s should be %s and %s", (name, expectedName, expectedSwarmId) => {
const c = new Container("id", new Date(), "image", name!, "command", "host", {}, "status", "created", []);
expect(c.name).toBe(expectedName);
expect(c.swarmId).toBe(expectedSwarmId);
});
});
+39 -15
View File
@@ -2,9 +2,7 @@ import type { ContainerHealth, ContainerStat, ContainerState } from "@/types/Con
import { useExponentialMovingAverage, useSimpleRefHistory } from "@/utils";
import { Ref } from "vue";
type Stat = Omit<ContainerStat, "id">;
const SWARM_ID_REGEX = /(\.[a-z0-9]{25})+$/i;
export type Stat = Omit<ContainerStat, "id">;
const hosts = computed(() =>
config.hosts.reduce(
@@ -16,36 +14,38 @@ const hosts = computed(() =>
),
);
export class GroupedContainers {
constructor(
public readonly name: string,
public readonly containers: Container[],
) {}
}
export class Container {
private _stat: Ref<Stat>;
private readonly _statsHistory: Ref<Stat[]>;
public readonly swarmId: string | null = null;
public readonly isSwarm: boolean = false;
private readonly movingAverageStat: Ref<Stat>;
private readonly _name: string;
constructor(
public readonly id: string,
public readonly created: Date,
public readonly image: string,
public readonly name: string,
name: string,
public readonly command: string,
public readonly host: string,
public readonly labels = {} as Record<string, string>,
public status: string,
public state: ContainerState,
stats: Stat[],
public readonly group?: string,
public health?: ContainerHealth,
) {
this._stat = ref({ cpu: 0, memory: 0, memoryUsage: 0 });
this._statsHistory = useSimpleRefHistory(this._stat, { capacity: 300, deep: true, initial: stats });
this._stat = ref(stats.at(-1) || ({ cpu: 0, memory: 0, memoryUsage: 0 } as Stat));
const { history } = useSimpleRefHistory(this._stat, { capacity: 300, deep: true, initial: stats });
this._statsHistory = history;
this.movingAverageStat = useExponentialMovingAverage(this._stat, 0.2);
const match = name.match(SWARM_ID_REGEX);
if (match) {
this.swarmId = match[0];
this.name = name.replace(`${this.swarmId}`, "");
this.isSwarm = true;
}
this._name = name;
}
get statsHistory() {
@@ -68,6 +68,30 @@ export class Container {
return `${stripVersion(this.image)}:${this.command}`;
}
get namespace() {
return this.labels["com.docker.stack.namespace"] || this.labels["com.docker.compose.project"];
}
get customGroup() {
return this.group;
}
get name() {
return this.isSwarm
? this.labels["com.docker.swarm.task.name"]
.replace(`.${this.labels["com.docker.swarm.task.id"]}`, "")
.replace(`.${this.labels["com.docker.swarm.node.id"]}`, "")
: this._name;
}
get swarmId() {
return this.labels["com.docker.swarm.task.name"].replace(this.name + ".", "");
}
get isSwarm() {
return Boolean(this.labels["com.docker.swarm.service.id"]);
}
public updateStat(stat: Stat) {
if (isRef(this._stat)) {
this._stat.value = stat;
+35 -17
View File
@@ -2,7 +2,7 @@ import { Component, ComputedRef, Ref } from "vue";
import { flattenJSON, getDeep } from "@/utils";
import ComplexLogItem from "@/components/LogViewer/ComplexLogItem.vue";
import SimpleLogItem from "@/components/LogViewer/SimpleLogItem.vue";
import DockerEventLogItem from "@/components/LogViewer/DockerEventLogItem.vue";
import ContainerEventLogItem from "@/components/LogViewer/ContainerEventLogItem.vue";
import SkippedEntriesLogItem from "@/components/LogViewer/SkippedEntriesLogItem.vue";
export type JSONValue = string | number | boolean | JSONObject | Array<JSONValue>;
@@ -17,12 +17,14 @@ export interface LogEvent {
readonly l: Level;
readonly p: Position;
readonly s: "stdout" | "stderr" | "unknown";
readonly c: string;
}
export abstract class LogEntry<T extends string | JSONObject> {
protected readonly _message: T;
constructor(
message: T,
public readonly containerID: string,
public readonly id: number,
public readonly date: Date,
public readonly std: Std,
@@ -41,13 +43,14 @@ export abstract class LogEntry<T extends string | JSONObject> {
export class SimpleLogEntry extends LogEntry<string> {
constructor(
message: string,
containerID: string,
id: number,
date: Date,
public readonly level: Level,
public readonly position: Position,
public readonly std: Std,
) {
super(message, id, date, std, level);
super(message, containerID, id, date, std, level);
}
getComponent(): Component {
return SimpleLogItem;
@@ -55,23 +58,27 @@ export class SimpleLogEntry extends LogEntry<string> {
}
export class ComplexLogEntry extends LogEntry<JSONObject> {
private readonly filteredMessage: ComputedRef<JSONObject>;
private readonly filteredMessage: ComputedRef<Record<string, any>>;
constructor(
message: JSONObject,
containerID: string,
id: number,
date: Date,
public readonly level: Level,
public readonly std: Std,
visibleKeys?: Ref<string[][]>,
visibleKeys?: Ref<Map<string[], boolean>>,
) {
super(message, id, date, std, level);
super(message, containerID, id, date, std, level);
if (visibleKeys) {
this.filteredMessage = computed(() => {
if (!visibleKeys.value.length) {
if (visibleKeys.value.size === 0) {
return flattenJSON(message);
} else {
return visibleKeys.value.reduce((acc, attr) => ({ ...acc, [attr.join(".")]: getDeep(message, attr) }), {});
const keys = Array.from(visibleKeys.value.entries())
.filter(([, value]) => value)
.map(([key]) => key);
return keys.reduce((acc, attr) => ({ ...acc, [attr.join(".")]: getDeep(message, attr) }), {});
}
});
} else {
@@ -82,29 +89,38 @@ export class ComplexLogEntry extends LogEntry<JSONObject> {
return ComplexLogItem;
}
public get message(): JSONObject {
return this.filteredMessage.value;
public get message(): Record<string, any> {
return unref(this.filteredMessage);
}
public get unfilteredMessage(): JSONObject {
return this._message;
}
static fromLogEvent(event: ComplexLogEntry, visibleKeys: Ref<string[][]>): ComplexLogEntry {
return new ComplexLogEntry(event._message, event.id, event.date, event.level, event.std, visibleKeys);
static fromLogEvent(event: ComplexLogEntry, visibleKeys: Ref<Map<string[], boolean>>): ComplexLogEntry {
return new ComplexLogEntry(
event._message,
event.containerID,
event.id,
event.date,
event.level,
event.std,
visibleKeys,
);
}
}
export class DockerEventLogEntry extends LogEntry<string> {
export class ContainerEventLogEntry extends LogEntry<string> {
constructor(
message: string,
containerID: string,
date: Date,
public readonly event: "container-stopped" | "container-started",
) {
super(message, date.getTime(), date, "stderr", "info");
super(message, containerID, date.getTime(), date, "stderr", "info");
}
getComponent(): Component {
return DockerEventLogItem;
return ContainerEventLogItem;
}
}
@@ -118,7 +134,7 @@ export class SkippedLogsEntry extends LogEntry<string> {
public readonly firstSkipped: LogEntry<string | JSONObject>,
lastSkipped: LogEntry<string | JSONObject>,
) {
super("", date.getTime(), date, "stderr", "info");
super("", "", date.getTime(), date, "stderr", "info");
this._totalSkipped = totalSkipped;
this.lastSkipped = lastSkipped;
}
@@ -148,19 +164,21 @@ export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
if (isObject(event.m)) {
return new ComplexLogEntry(
event.m,
event.c,
event.id,
new Date(event.ts),
event.l,
event.s === "unknown" ? "stderr" : event.s ?? "stderr",
event.s === "unknown" ? "stderr" : (event.s ?? "stderr"),
);
} else {
return new SimpleLogEntry(
event.m,
event.c,
event.id,
new Date(event.ts),
event.l,
event.p,
event.s === "unknown" ? "stderr" : event.s ?? "stderr",
event.s === "unknown" ? "stderr" : (event.s ?? "stderr"),
);
}
}

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