Compare commits

...

491 Commits

Author SHA1 Message Date
Amir Raminfar 11451afc9a chore: release v4.11.2 2023-08-24 10:57:50 -07:00
codeman1o1 efb07b340a Fix bad version update URL (#2364) 2023-08-24 10:54:34 -07:00
Amir Raminfar ba51793695 chore: updates go modules (#2361) 2023-08-22 17:41:01 +00:00
Amir Raminfar af4556d0ca chore: release v4.11.1 2023-08-21 10:06:32 -07:00
Amir Raminfar d041767455 fix: cleans up storage to use only one key (#2360) 2023-08-21 10:04:16 -07:00
Amir Raminfar 69676c7aaa chore: cleans up refs with toRefs from vueuse (#2359) 2023-08-21 09:27:37 -07:00
renovate[bot] d340e07d8a fix(deps): update all non-major dependencies 2023-08-21 01:16:42 +00:00
Amir Raminfar 0cd8b3d900 feat: remembers the sort and direction for containers table on homepage (#2357) 2023-08-20 22:24:04 +00:00
Amir Raminfar 26d2f71102 feat(minor): updates colors on hover for anchors 2023-08-20 14:25:24 -07:00
Amir Raminfar f7740ebd04 fix: fixes broken animation on scroll button (#2356) 2023-08-18 22:18:23 +00:00
Amir Raminfar a150b44bec fix: fixes negative scrollvalues 2023-08-18 12:54:45 -07:00
Amir Raminfar 54610b0f5a fix: fixes scroll progress showing temporary (#2355) 2023-08-18 12:48:17 -07:00
Amir Raminfar 7f1303ed4d chore: cleans code and uses css for animation (#2354) 2023-08-18 11:23:22 -07:00
Amir Raminfar 7ea82ee2fd fix: adds keyboard short component to settings page (#2353) 2023-08-18 10:32:58 -07:00
Amir Raminfar c178f7be8a feat: uses a different color when hovering on links (#2352) 2023-08-16 14:07:51 -07:00
Amir Raminfar 81be24eccd chore: release v4.11.0 2023-08-16 11:21:07 -07:00
Amir Raminfar c51df3b4e7 feat: hides swarm id on title and animates it on hover (#2351) 2023-08-16 18:19:44 +00:00
Amir Raminfar 076f62bac7 feat: allows containers to be pinned (#2350)
* feat: allows containers to be pinned

* adds animation

* clean up

* updates int test

* fixes tests
2023-08-15 14:16:32 -07:00
Amir Raminfar 2ec0d7bd4e chore: code clean up (#2349) 2023-08-14 10:49:43 -07:00
renovate[bot] 9971bf0d3f chore(deps): update dependency lint-staged to v14 (#2348)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-14 08:50:43 -07:00
renovate[bot] d8c47a69c7 chore(deps): update all non-major dependencies 2023-08-14 02:03:57 +00:00
Amir Raminfar 14edf1b404 feat: adds keyboard shortcuts visually (#2344)
* feat: adds keyboard shortcuts visually

* Adds missing file

* uses tags instead

* removes @vueuse/math

* fixes background
2023-08-11 12:53:02 -07:00
Amir Raminfar 14dd1af6df feat: warns against unexpected env variables (#2345) 2023-08-11 10:23:24 -07:00
Amir Raminfar 2b2d0ed97f chore: release v4.10.26 2023-08-10 08:22:56 -07:00
Amir Raminfar e26a60ffa4 fix: fixes download link when searching. see #2342 (#2343) 2023-08-10 08:22:21 -07:00
Amir Raminfar 5946e56ccb chore: release v4.10.25 2023-08-09 09:42:58 -07:00
Amir Raminfar 8f77400f8b fix: reverts unneeded css files from oruga (#2341) 2023-08-09 09:41:09 -07:00
renovate[bot] 19cc2f30c4 chore(deps): update dependency unplugin-vue-macros to ^2.4.4 2023-08-07 18:50:53 +00:00
Amir Raminfar 298993289c chore: changes renovate to bump 2023-08-07 11:44:33 -07:00
Amir Raminfar a06f197015 chore: changes renovate to pin 2023-08-07 11:43:29 -07:00
Amir Raminfar 39917518b9 chore: updates modules 2023-08-07 09:34:46 -07:00
Amir Raminfar cbccc1ac4c feat: shows running / total containers (#2339) 2023-08-07 09:34:08 -07:00
renovate[bot] 5cc5e517e6 fix(deps): update all non-major dependencies 2023-08-07 00:21:43 +00:00
Amir Raminfar 44e21cba83 chore: cleans up types in typescript (#2335) 2023-08-03 09:23:58 -07:00
Amir Raminfar ac886b0e9a feat: adds bar charts back to dashboard table (#2331) 2023-08-02 11:29:50 -07:00
Amir Raminfar 1c74a46f3f chore: release v4.10.24 2023-08-01 12:42:22 -07:00
Amir Raminfar 8778500aa6 chore: updates modules 2023-08-01 12:36:08 -07:00
Amir Raminfar 3eb222066c feat: updates home dashboard to support mobile view (#2334)
* feat: updates home dashboard to support mobile view

* updates created translation

* adds ellipses
2023-08-01 12:35:05 -07:00
yldrmali 5d326189a8 fix: cap scrollProgress value to 1. (#2333) 2023-08-01 09:32:30 -07:00
Amir Raminfar b722fae909 chore: release v4.10.23 2023-07-31 10:12:48 -07:00
Amir Raminfar 8e21cbd51c fix: changes default direction to desc 2023-07-31 10:06:49 -07:00
Amir Raminfar 166e547f05 chore(cleanup): cleans up if statements 2023-07-31 09:57:47 -07:00
Amir Raminfar ada1a98d9c chore: removes console 2023-07-31 09:55:39 -07:00
Amir Raminfar 40c259b1be feat: adds table sort to homepage (#2330)
* feat: adds table sort to homepage

* feat: adds sorting to dashboard

* fixes colors
2023-07-31 16:54:27 +00:00
renovate[bot] ac0b9b157e chore(deps): update all non-major dependencies 2023-07-31 00:14:29 +00:00
Amir Raminfar 0be7005ab4 feat: adds pagination for dashboard page. see #2322 (#2328) 2023-07-28 20:30:15 +00:00
Amir Raminfar 97f50d696c fix: fixes JSON encoding when nested array is found. see #2325 (#2327) 2023-07-28 20:16:05 +00:00
Amir Raminfar 3c53e218d1 chore: release v4.10.22 2023-07-27 19:21:05 -07:00
Amir Raminfar 11487003f2 fix(performance): remove table component and uses own table component to improve performance (#2326)
* fix: remove table component and uses own table component to improve performance

* adds id
2023-07-28 02:20:23 +00:00
Amir Raminfar c1b8d0aed0 chore: release v4.10.21 2023-07-27 10:25:29 -07:00
Amir Raminfar 699181ec7c fix: removes sorting from homepage temporary until a better fix is found (#2323)
* fix: removes sorting from homepage temporary until a better fix is found

* adds pagination and remove bar charts
2023-07-27 10:21:32 -07:00
Amir Raminfar 62f9da0536 chore: release v4.10.20 2023-07-26 10:08:21 -07:00
Amir Raminfar 10de475a64 fix: adds new languages for dashboard (#2320) 2023-07-26 17:07:18 +00:00
Amir Raminfar 14bf3a1417 chore: updates modules (#2321) 2023-07-26 17:06:55 +00:00
Amir Raminfar 0ff031e361 chore: adds visual comparison for aside menu only (#2319)
* chore: adds visual comparison for aside menu only

* adds missing files
2023-07-26 10:03:07 -07:00
Amir Raminfar baf3cc70f8 chore: updates modules 2023-07-25 09:28:38 -07:00
Amir Raminfar 1b056346c0 feat: adds exponential average and a new dashboard showing all containers (#2317)
* chore: updates modules

* feat: adds support for exponential moving average

* feat: add expoentital moving avg

* adds index page

* cleans up table

* fixes typecheck

* adds bar chart

* updates dashboard to orgua table

* wip

* cleans up ui

* remove screenshot tests for playwright

* adds more tests

* fixes icon

* fixes default sort

* removes unused var

* adds vscode settings
2023-07-24 15:06:42 -07:00
Amir Raminfar 18005fcd23 chore: updates modules 2023-07-24 08:40:48 -07:00
renovate[bot] 425ab449a6 chore(deps): update all non-major dependencies 2023-07-24 02:10:38 +00:00
Amir Raminfar ff0ceea29d chore: fixes renovate json 2023-07-21 12:07:45 -07:00
renovate[bot] 5b509d26d4 chore(deps): update dependency vite to v4.4.6 2023-07-21 11:34:03 +00:00
renovate[bot] f2dbda3dde chore(deps): update dependency vite to v4.4.5 2023-07-20 15:13:50 +00:00
Amir Raminfar 6b1b7099b1 go cleanup (#2313)
* chore: cleans waitgroups

* chore: adds benchmark
2023-07-18 10:58:59 -07:00
Amir Raminfar 93347a6888 chore(docs): improve top menu on home layout 2023-07-18 08:40:00 -07:00
renovate[bot] 80800674bd chore(deps): update pnpm to v8.6.9 2023-07-18 13:14:58 +00:00
Amir Raminfar 6f8cf9045e chore(docs): adds link to github on homepage 2023-07-17 14:31:54 -07:00
renovate[bot] 5988b1d0df chore(deps): update pnpm to v8.6.8 2023-07-17 12:11:22 +00:00
Amir Raminfar c774ba7edb chore: adds more gotests 2023-07-15 13:07:05 -07:00
Amir Raminfar 079ac56442 fix: uses json.valid instead 2023-07-15 12:25:16 -07:00
renovate[bot] 178ade4d20 chore(deps): update mcr.microsoft.com/playwright docker tag to v1.36.1 2023-07-15 01:05:29 +00:00
Amir Raminfar ffa1fb976e chore(test): updates playwright (#2310) 2023-07-14 10:22:33 -07:00
renovate[bot] b0b1793874 chore(deps): update dependency vite to v4.4.4 2023-07-14 07:33:44 +00:00
renovate[bot] 30491b10ff fix(deps): update module github.com/go-chi/chi/v5 to v5.0.10 2023-07-13 16:46:21 +00:00
Amir Raminfar 991588a42e chore(typo): fixes typo in docs 2023-07-12 14:29:04 -07:00
Amir Raminfar 1a899a18e8 chore: release v4.10.19 2023-07-12 09:47:21 -07:00
Amir Raminfar 53ad92c60d chore(docs): adds a page about logs on file 2023-07-12 09:39:02 -07:00
renovate[bot] a9eb17cce3 chore(deps): update docker/setup-buildx-action action to v2.9.1 2023-07-12 12:51:08 +00:00
Amir Raminfar 8c8987b666 chore(refactor): refactors docker client for better testing (#2302)
* chore(refactor): refactors docker client for better testing

* more refactoring and clenaing up tests
2023-07-11 13:21:46 -07:00
renovate[bot] bbc6853f96 chore(deps): update golang docker tag to v1.20.6 2023-07-11 19:54:26 +00:00
Amir Raminfar c083c8f0c9 chore: updates modules (#2300) 2023-07-11 10:34:09 -07:00
renovate[bot] 33e5a1560f chore(deps): update dependency vite to v4.4.3 2023-07-11 16:57:01 +00:00
Amir Raminfar 1bb1081f1a fix(performance): improves streaming of logs by using the Docker library to demultiplex (#2295)
* fix(performance): improves streaming of logs by using the Docker library to demultiplex

* fixes go tests

* changes channel to unbuffered

* fixes golang race test

* fixes stopped containers

* wip

* uses different strategy

* cleans tests

* cleans tests

* adds more tests

* fixes possible bug

* fixes download

* adds more tests for download

* uses pool

* adds more tests
2023-07-11 09:52:30 -07:00
renovate[bot] ad7b3a2b52 chore(deps): update pnpm to v8.6.7 2023-07-09 22:59:02 +00:00
renovate[bot] e00c918025 fix(deps): update module github.com/docker/docker to v24.0.4+incompatible 2023-07-08 05:13:46 +00:00
renovate[bot] 35455d845a chore(deps): update docker/setup-buildx-action action to v2.9.0 (#2294)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-07 08:44:31 -07:00
renovate[bot] 4b73fb8413 chore(deps): update dependency vite to v4.4.2 2023-07-07 13:41:22 +00:00
renovate[bot] fc97b1dcf9 fix(deps): update module github.com/docker/docker to v24.0.3+incompatible (#2292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-06 12:50:37 -07:00
renovate[bot] b7085dda2b chore(deps): update dependency vite to v4.4.1 (#2290)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-06 11:43:15 -07:00
Amir Raminfar 901785dc20 chore: release v4.10.18 2023-07-06 09:30:22 -07:00
Amir Raminfar 2c909dab09 fix: adds port and schema to connection again to fix clashes between same host. fixes #2279 (#2289)
* fix: adds port and schema to connection again to fix clashes between same host. fixes #2279

* fixes tests
2023-07-06 09:22:22 -07:00
Amir Raminfar e4ef784be7 chore(format): updates js files to updates prettier 2023-07-06 08:41:04 -07:00
Amir Raminfar d2bb0cd31d chore: updates modules 2023-07-06 08:39:42 -07:00
renovate[bot] 0562b60df4 chore(deps): update dependency prettier to v3 (#2286)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-06 08:32:01 -07:00
renovate[bot] d1e63155c9 chore(deps): update dependency vitest to ^0.33.0 (#2288)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-06 08:30:06 -07:00
renovate[bot] 7fed87fa79 chore(deps): update dependency vite to v4.4.0 2023-07-06 14:46:02 +00:00
renovate[bot] 90c4b8a68b chore(deps): update pnpm to v8.6.6 2023-07-03 18:49:48 +00:00
renovate[bot] c85ce593a1 chore(deps): update dependency vitepress to v1.0.0-beta.5 2023-07-03 09:14:05 +00:00
renovate[bot] 0fe9278a29 chore(deps): update dependency vitepress to v1.0.0-beta.4 2023-07-02 19:14:15 +00:00
renovate[bot] 8a616b6bc0 fix(deps): update dependency @intlify/unplugin-vue-i18n to ^0.12.0 2023-07-01 22:38:37 +00:00
renovate[bot] f3750fd7d0 chore(deps): update docker/setup-buildx-action action to v2.8.0 2023-06-28 21:56:04 +00:00
Amir Raminfar 5af162ddda chore: updates modules 2023-06-28 12:31:33 -07:00
Amir Raminfar b2a6ea7ba1 fix: improves performance by making initital requests in parallel (#2277) 2023-06-28 12:26:47 -07:00
Amir Raminfar 70acaa64d8 feat: adds ability to support labels with | delimeter (#2276)
* feat: adds ability to support labels with | delimeter

* fixes tests

* updates docs
2023-06-28 12:14:17 -07:00
Amir Raminfar f6807db592 chore: release v4.10.17 2023-06-27 15:43:22 -07:00
Amir Raminfar 6e394bad19 fix: fixes download link not working (#2274) 2023-06-27 13:25:51 -07:00
Amir Raminfar 657e6314c0 chore: release v4.10.16 2023-06-27 11:54:43 -07:00
Amir Raminfar a6fff56997 fix: cleans up config in template (#2273) 2023-06-27 11:35:55 -07:00
Amir Raminfar 05a4b7c25a feat(dev): moves vite integeration to backend (#2272)
* feat(dev): moves vite integeration to backend

* fixes dockerbuild

* fixes docker build

* fixes custom base

* fixes base path again

* uses concurrently instead

* fixes go tests
2023-06-27 09:20:24 -07:00
renovate[bot] 263029d77e chore(deps): update pnpm to v8.6.5 2023-06-26 13:47:57 +00:00
renovate[bot] 32be87d245 chore(deps): update pnpm to v8.6.4 2023-06-26 03:30:58 +00:00
Amir Raminfar 14fc1190a8 feat: supports multiple hosts in parallel and update hosts menu (#2269)
* feat: updates host menu to be part of side menu

* updates routes to be host/id

* fixes go tests

* fixes js tests

* fixes typescheck

* fixes int tests

* fixes mobile

* fixes bug in merging containers

* fixed minor bug with menu
2023-06-24 19:13:39 +00:00
Amir Raminfar 9f90d1ccfa chore: release v4.10.15 2023-06-20 15:43:09 -07:00
Amir Raminfar 8b3ddcfca7 chore: adds more e2e tests (#2266) 2023-06-20 15:36:33 -07:00
Amir Raminfar 50f3c0b7a4 fix: fixes UI when no localhost is provided 2023-06-20 14:38:12 -07:00
Amir Raminfar 31063fa1b4 feat: removes localhost as a required client. fixes #2259 (#2263)
* feat: removes localhost as a required connection

* refactors code

* fixes tests

* adds more tests

* adds more tests

* refactors

* cleans up logs
2023-06-20 17:43:47 +00:00
renovate[bot] 3aa874e399 chore(deps): update dependency vitepress to v1.0.0-beta.3 2023-06-20 13:19:07 +00:00
renovate[bot] ef7bb3fbcb chore(deps): update pnpm to v8.6.3 (#2262)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-19 12:59:34 -07:00
Amir Raminfar a019705bae fix: downgrade json parsing errors to warn. see #2260 2023-06-19 08:32:32 -07:00
Amir Raminfar e59f9106cc fix: fixes route update with hash 2023-06-18 13:35:04 -07:00
Amir Raminfar b3357e9843 chore(logs): prints the original log when json unmarshalling fails 2023-06-18 13:25:26 -07:00
Amir Raminfar 3511739008 chore: updates modules 2023-06-18 13:24:30 -07:00
renovate[bot] 608a39d841 chore(deps): update mcr.microsoft.com/playwright docker tag to v1.35.1 2023-06-15 21:47:49 +00:00
renovate[bot] 360985e8e1 chore(deps): update docker/setup-buildx-action action to v2.7.0 2023-06-14 04:19:01 +00:00
renovate[bot] 82f78582b5 chore(deps): update pnpm to v8.6.2 2023-06-14 01:37:18 +00:00
renovate[bot] 10bcb7812a chore(deps): update docker/build-push-action action to v4.1.1 (#2255)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-13 12:50:19 -07:00
Amir Raminfar a7693f8a6b chore: updates modules and playwright (#2253) 2023-06-13 12:26:36 -07:00
Amir Raminfar 93297649bc chore: release v4.10.14 2023-06-09 14:15:39 -07:00
renovate[bot] d25f87390c chore(deps): update docker/build-push-action action to v4.1.0 2023-06-09 14:31:59 +00:00
Amir Raminfar 5a713dc88c feat: adds chinese translations (#2249)
* feat: adds chinese translations

* updates language for zh
2023-06-08 22:38:13 +00:00
Amir Raminfar b23daa4158 chore: release v4.10.13 2023-06-08 11:28:01 -07:00
Amir Raminfar 41da3f0de4 fix: fixes searching non-ascii characters by also escaping search entities (#2248) 2023-06-08 18:25:26 +00:00
Amir Raminfar cb5712f7e0 chore: fixes german yaml file 2023-06-08 11:21:04 -07:00
Amir Raminfar 69723962fd fix: german language for buttons was incorrect 2023-06-08 11:14:28 -07:00
Amir Raminfar 81cfdbec42 feat: adds german to locales 2023-06-08 10:38:03 -07:00
Amir Raminfar 7477b1aecc fix: fixes std stream when type is unknown. see #2243 (#2247)
* fix: fixes std stream when type is unknown. see #2243

* fixes tests
2023-06-08 10:23:02 -07:00
Amir Raminfar 8912339044 chore: fixes import 2023-06-07 12:07:39 -07:00
Amir Raminfar 3f328a0ebf chore: removes logging for routes 2023-06-07 11:28:33 -07:00
renovate[bot] 4ea51fc06f chore(deps): update docker/login-action action to v2.2.0 (#2244)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-07 10:02:44 -07:00
renovate[bot] f4ebd12fbf chore(deps): update docker/setup-buildx-action action to v2.6.0 (#2245)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-07 10:02:32 -07:00
Amir Raminfar 8df12a4f3d chore: updates modules 2023-06-07 09:53:28 -07:00
renovate[bot] ab8441ac01 chore(deps): update golang docker tag to v1.20.5 2023-06-07 02:39:31 +00:00
renovate[bot] 17125c67ed chore(deps): update dependency vitest to ^0.32.0 2023-06-06 22:22:46 +00:00
renovate[bot] 8fd8e82994 chore(deps): update pnpm to v8.6.1 2023-06-05 18:18:53 +00:00
renovate[bot] 31ddc941ff chore(deps): update dependency vite-plugin-pages to ^0.31.0 2023-06-05 11:44:53 +00:00
renovate[bot] 94d4610b08 chore(deps): update dependency unocss to ^0.53.0 2023-06-05 03:29:19 +00:00
renovate[bot] d9a810111b chore(deps): update dependency @unocss/transformer-directives to ^0.53.0 2023-06-05 02:59:22 +00:00
renovate[bot] bdf77b7f8a chore(deps): update dependency @unocss/reset to ^0.53.0 2023-06-04 22:28:42 +00:00
renovate[bot] 19c2bc8274 chore(deps): update dependency @unocss/preset-typography to ^0.53.0 2023-06-04 19:39:49 +00:00
renovate[bot] b431ce16c5 fix(deps): update module github.com/sirupsen/logrus to v1.9.3 2023-06-04 00:37:51 +00:00
Amir Raminfar 38a6a99096 fix: fixes nested array displaying as object instead of actual value (#2233) 2023-06-02 15:33:59 -07:00
Amir Raminfar 88442edee5 chore(deps): remove gorilla mux in favor of using go-chi (#2232)
* chore(deps): remove gorilla mux in favor of using go-chi

* fixes broken redirect and tests

* chore: clean up

* reverts changes
2023-06-02 11:55:06 -07:00
Amir Raminfar d7710f736f chore: fixes typing for emits 2023-06-01 15:24:41 -07:00
Amir Raminfar c9bb793e9b chore: release v4.10.12 2023-06-01 10:16:09 -07:00
Amir Raminfar c522a66d1a fix: fixes stream colors by swapping (#2231) 2023-06-01 16:59:47 +00:00
Amir Raminfar d0edc42d1b feat: adds better icons for streams in the drop down (#2230) 2023-06-01 09:45:29 -07:00
Amir Raminfar 5f92e84d9d feat: adds support for different std out and err streams (#2229)
* feat: adds support for different std out and err streams

* feat: adds std to json

* fixes tests

* fixes deprecated code

* fixes download

* adds defineEmit as an option

* chore: updates modules

* adds ui elements

* fixes tests

* updates languages
2023-05-31 15:19:40 -07:00
renovate[bot] 84b8e24ca3 chore(deps): update dependency unplugin-vue-components to ^0.25.0 (#2228)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-30 10:11:32 -07:00
renovate[bot] c225a837df fix(deps): update module github.com/stretchr/testify to v1.8.4 2023-05-30 16:19:04 +00:00
renovate[bot] 3b6ad12c93 chore(deps): update pnpm to v8.6.0 2023-05-28 14:26:44 +00:00
Amir Raminfar 2581150346 chore: release v4.10.11 2023-05-26 12:09:21 -07:00
Amir Raminfar 3d93bb69cb fix: fixes cpu monitor when all values are 0. fixes #2215 (#2222)
* fix: fixes cpu monitor when all values are 0. fixes #2215

* chore: remove unused imports
2023-05-26 12:07:41 -07:00
Amir Raminfar cc7d834e35 feat: adds warning as possible level 2023-05-26 10:58:08 -07:00
renovate[bot] ecf5fda53e fix(deps): update dependency @intlify/unplugin-vue-i18n to ^0.11.0 (#2221)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-26 08:44:08 -07:00
Amir Raminfar 73aa1d9010 fix: improves graceful shutdown 2023-05-26 08:42:04 -07:00
renovate[bot] 066248c531 fix(deps): update module github.com/docker/docker to v24.0.2+incompatible (#2220)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-26 08:34:26 -07:00
renovate[bot] 8c29954caf chore(deps): update dependency vite to v4.3.9 (#2219)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-26 08:34:18 -07:00
Amir Raminfar ce63996954 chore: remove debug statement 2023-05-25 11:35:48 -07:00
Amir Raminfar ad46d53ef2 feat: removes colors to find the level (#2218) 2023-05-25 10:21:07 -07:00
renovate[bot] a2d208adf7 chore(deps): update mcr.microsoft.com/playwright docker tag to v1.34.3 2023-05-25 09:27:27 +00:00
Amir Raminfar c2110e322f chore: updates modules (#2216) 2023-05-24 10:11:01 -07:00
renovate[bot] d9376b303a chore(deps): update mcr.microsoft.com/playwright docker tag to v1.34.2 2023-05-24 05:27:49 +00:00
Amir Raminfar 3baed9da3d chore: release v4.10.10 2023-05-23 12:21:20 -07:00
Amir Raminfar 8a88b332c5 chore: fixes int test for release 2023-05-23 12:21:07 -07:00
Amir Raminfar 770f5e63ff chore: release v4.10.9 2023-05-23 12:19:05 -07:00
Amir Raminfar f0424e1e94 feat: shows error message when container is not found (#2213) 2023-05-23 12:11:54 -07:00
Amir Raminfar fc0c189e38 feat: adds a sub header to display stats on mobile view (#2212) 2023-05-23 12:07:19 -07:00
renovate[bot] 8586f16ea5 chore(deps): update mcr.microsoft.com/playwright docker tag to v1.34.1 2023-05-23 11:52:56 +00:00
Amir Raminfar 7427d005c8 chore: vue updates (#2211)
* chore: updates node modules

* chore: fixes reactivity warning
2023-05-22 18:51:50 +00:00
Amir Raminfar e7174dea69 chore: updates playwright 2023-05-22 11:48:50 -07:00
Amir Raminfar 5b12a3cc18 chore: updates node modules 2023-05-22 11:34:51 -07:00
Amir Raminfar b2844469d6 chore(e2e): updates testing to use playwright (#2181)
* chore(e2e): updates testing to use playwright

* chore: updates workflows

* fixes spaces

* fixes space again

* fixes int test to add docker

* chore: ignore e2e tests for vite

* updates screenshots for linux

* updates screenshots again

* chore: uses docker compose for e2e

* adds PWTEST_SKIP_TEST_OUTPUT

* add ci

* updates screenshots again

* updates with  css

* adds more tests

* updates tests
2023-05-22 11:33:25 -07:00
Amir Raminfar 0d3bd7246a chore: updates go modules 2023-05-22 10:20:10 -07:00
renovate[bot] d0c080c7ae chore(deps): update dependency vitepress to v1.0.0-beta.1 (#2209)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-22 10:12:18 -07:00
Amir Raminfar 48aa0afa3a chore: updates all other modules (#2208) 2023-05-21 09:50:13 -07:00
renovate[bot] a9ff1e9335 chore(deps): update dependency @unocss/reset to ^0.52.0 (#2207) 2023-05-21 07:37:32 -07:00
renovate[bot] 9eb0242544 fix(deps): update module github.com/docker/docker to v24.0.1+incompatible 2023-05-20 05:00:43 +00:00
renovate[bot] 6b41b7acef chore(deps): update dependency vite-plugin-pages to ^0.30.0 2023-05-19 11:44:56 +00:00
renovate[bot] d80d8d4484 fix(deps): update module github.com/stretchr/testify to v1.8.3 2023-05-19 04:33:01 +00:00
renovate[bot] d268d3e2ca chore(deps): update dependency vitepress to v1.0.0-alpha.76 2023-05-18 21:55:35 +00:00
Agneev Mukherjee d867c08aae fix: use ui-monospace as the default font for log viewer (#2200)
* Use ui-monospace as the default font for log body

* Add SF Mono
2023-05-18 07:38:55 -07:00
renovate[bot] 969fdf62d0 chore(deps): update dependency vite to v4.3.8 (#2199)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-18 07:36:14 -07:00
renovate[bot] 8d3f501bac fix(deps): update module github.com/sirupsen/logrus to v1.9.2 2023-05-18 00:48:57 +00:00
renovate[bot] 9ab7f0acca fix(deps): update module github.com/docker/docker to v24 (#2196) 2023-05-16 20:54:50 -07:00
renovate[bot] ce4ab3a249 fix(deps): update module github.com/sirupsen/logrus to v1.9.1 2023-05-17 03:49:30 +00:00
renovate[bot] 82e37c7397 chore(deps): update dependency vite to v4.3.7 2023-05-17 01:35:58 +00:00
renovate[bot] ea23891ac6 chore(deps): update dependency unplugin-auto-import to ^0.16.0 2023-05-16 12:18:22 +00:00
renovate[bot] 0e9edd8bf2 chore(deps): update dependency vite to v4.3.6 2023-05-15 21:13:02 +00:00
renovate[bot] 23c53a8e3a chore(deps): update pnpm to v8.5.1 2023-05-15 05:33:58 +00:00
Amir Raminfar 86fc556fb9 chore: release v4.10.8 2023-05-12 12:44:44 -07:00
renovate[bot] f5f268b677 chore(deps): update dependency @vitejs/plugin-vue to v4.2.3 (#2190)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-12 08:25:34 -07:00
Amir Raminfar 352d815255 Revert "chore: updates vue"
This reverts commit e7e2089f4f.
2023-05-11 13:33:56 -07:00
Amir Raminfar e7e2089f4f chore: updates vue 2023-05-11 11:42:01 -07:00
Amir Raminfar d4762908be fix: fixes incorrect regex group match. fixes #2187 (#2188) 2023-05-11 18:36:45 +00:00
renovate[bot] 71f9963a75 chore(deps): update dependency @vitejs/plugin-vue to v4.2.2 2023-05-11 15:43:39 +00:00
Amir Raminfar 5056298704 chore: updates modules 2023-05-10 12:19:07 -07:00
Amir Raminfar 491b1cb944 chore: release v4.10.7 2023-05-10 12:15:05 -07:00
Amir Raminfar b7d5229534 fix: fixes logic for extracing swarm ids. fixed #2184 (#2185) 2023-05-10 10:54:41 -07:00
renovate[bot] 1ffa17ef48 chore(deps): update cypress/included docker tag to v12.12.0 2023-05-10 06:04:38 +00:00
renovate[bot] b55243a6b9 chore(deps): update pnpm to v8.5.0 2023-05-10 02:29:33 +00:00
renovate[bot] 002d8dedee chore(deps): update dependency @pinia/testing to ^0.1.0 2023-05-09 00:57:26 +00:00
renovate[bot] 4348ba6323 fix(deps): update module github.com/docker/docker to v23.0.6+incompatible 2023-05-08 21:20:51 +00:00
Amir Raminfar 0b76f1185b chore: release v4.10.6 2023-05-08 11:03:04 -07:00
Amir Raminfar 4fc8109048 feat: respects fg color with css color using ansi (#2180) 2023-05-08 11:02:30 -07:00
Amir Raminfar e0e4292d1c chore: release v4.10.5 2023-05-08 09:19:42 -07:00
Amir Raminfar 18c57066c7 fix: fixes colors on side menu to be less bold and updates the regex to support longer hashes. see #2144 (#2177) 2023-05-08 09:17:34 -07:00
renovate[bot] 6f1b3a1dd1 chore(deps): update dependency @vue/compiler-sfc to ^3.3.0-beta 2023-05-08 12:10:46 +00:00
Amir Raminfar 370e7b238e fix: shows a message when all keys in json logs are hidden. see #2122 (#2174) 2023-05-05 14:54:37 -07:00
Amir Raminfar f89e2cc6a7 chore: tidy go mod 2023-05-05 11:26:17 -07:00
Amir Raminfar 049ae108de chore: cleans up types 2023-05-05 10:07:29 -07:00
renovate[bot] be47fdab12 chore(deps): update dependency vite to v4.3.5 2023-05-05 14:03:20 +00:00
renovate[bot] d0ae1db50b chore(deps): update dependency vitest to ^0.31.0 2023-05-04 03:08:50 +00:00
Amir Raminfar 6b32920d33 chore: cleans up types for config 2023-05-03 10:11:09 -07:00
renovate[bot] 25d7c14783 chore(deps): update golang docker tag to v1.20.4 2023-05-02 23:40:02 +00:00
Amir Raminfar 7c13637870 chore: release v4.10.4 2023-05-02 09:31:03 -07:00
renovate[bot] 1b969380bc chore(deps): update pnpm to v8.4.0 (#2167)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-02 16:11:09 +00:00
Amir Raminfar cfe616a79e feat: improves popup timing (#2166) 2023-05-02 09:09:13 -07:00
Amir Raminfar 764d176455 feat: moves mobile host to mobile menu. see #2157 (#2165)
* Revert "feat: adds host drop down to mobile view. see #2157 (#2160)"

This reverts commit 088d96cb0d.

* feat: moves mobiles host drop down to mobile menu
2023-05-02 16:08:26 +00:00
Amir Raminfar 786aaedc78 chore: updates npm modules 2023-05-02 09:06:13 -07:00
renovate[bot] 847daed5e8 chore(deps): update dependency jsdom to v22 (#2164)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-02 09:03:51 -07:00
renovate[bot] f410b9dd1d chore(deps): update dependency vite to v4.3.4 2023-05-02 12:22:45 +00:00
renovate[bot] 0b83fb4356 chore(deps): update dependency vitepress to v1.0.0-alpha.75 2023-04-30 14:37:47 +00:00
Amir Raminfar 088d96cb0d feat: adds host drop down to mobile view. see #2157 (#2160)
* feat: adds host drop down to mobile view. see #2157

* fix: removes unused imports
2023-04-27 16:32:48 -07:00
Amir Raminfar 9aed4ba19f chore: release v4.10.3 2023-04-27 16:24:22 -07:00
Amir Raminfar 30a4d9a6be chore: updates all modules 2023-04-27 16:09:45 -07:00
Amir Raminfar 36b4bb65e7 feat: adds a new popup to show uptime and stats on the side menu (#2159) 2023-04-27 12:47:23 -07:00
Amir Raminfar 8767d3461d Merge branch 'master' into tooltips 2023-04-27 12:40:33 -07:00
renovate[bot] cd9f5971a4 chore(deps): update cypress/included docker tag to v12.11.0 2023-04-27 19:39:54 +00:00
Amir Raminfar 3c7021af42 fix: removes unused imports 2023-04-27 12:37:36 -07:00
Amir Raminfar 0b9b9f12da chore: removes tooltip from bulma 2023-04-27 12:35:28 -07:00
Amir Raminfar 16eb71e0c0 chore: cleans up d.ts file 2023-04-27 12:32:38 -07:00
Amir Raminfar 17dfcf3d9a chore(refactor): creates new popup component for containers 2023-04-27 12:31:43 -07:00
Amir Raminfar 2dbbe47c5c chore(refactor): creates new popup component for containers 2023-04-27 12:31:12 -07:00
Amir Raminfar 6076c4eb63 chore: cleans up code 2023-04-27 12:27:48 -07:00
Amir Raminfar 818fec244a chore(refactor): create docker health component 2023-04-27 12:23:13 -07:00
Amir Raminfar 1f85f046cf fix: uses getBoundingClientRect 2023-04-27 11:49:46 -07:00
Amir Raminfar e6efe6ff59 feat: adds on hover popup on the side menu 2023-04-27 11:07:08 -07:00
Amir Raminfar e96ab50486 feat: adds styles to popup 2023-04-27 09:31:19 -07:00
renovate[bot] f74b1c91f2 fix(deps): update module github.com/docker/docker to v23.0.5+incompatible 2023-04-27 12:12:24 +00:00
renovate[bot] 0cbfcb6097 chore(deps): update dependency vite to v4.3.3 2023-04-27 03:46:57 +00:00
Amir Raminfar 27d0244e5d fix: removes extra space between name and swarm id 2023-04-26 13:50:25 -07:00
Amir Raminfar 7b19e83fea chore: release v4.10.2 2023-04-26 12:30:53 -07:00
Amir Raminfar 832594532a fix: fixes mobile nav not showing up when container does not exist. fixes #2153 2023-04-26 12:22:38 -07:00
renovate[bot] 298b39f6b9 chore(deps): update dependency @vitejs/plugin-vue to v4.2.1 2023-04-26 14:00:54 +00:00
renovate[bot] e04fbbb51a fix(deps): update dependency @oruga-ui/oruga-next to ^0.6.0 2023-04-26 08:12:39 +00:00
Amir Raminfar 685b17ef4e chore: release v4.10.1 2023-04-25 14:48:52 -07:00
Amir Raminfar 582aa207f1 chore: updates all modules (#2151) 2023-04-25 14:48:13 -07:00
Amir Raminfar 8116868669 feat: makes swarm id lighter (#2145)
* feat: makes swarm id lighter

* fix: add swarm to title id
2023-04-25 14:41:45 -07:00
renovate[bot] cd125e83be chore(deps): update dependency vite to v4.3.2 2023-04-25 20:23:14 +00:00
renovate[bot] 6ba0f6037f chore(deps): update dependency vitepress to v1.0.0-alpha.74 2023-04-25 08:37:32 +00:00
Amir Raminfar 5bc02e49f2 chore: release v4.10.0 2023-04-22 13:05:24 -07:00
Amir Raminfar bf634382e5 feat: sorts running conttainers at top (#2142) 2023-04-22 13:04:52 -07:00
Amir Raminfar 551fb1579d chore: removes semver in favor of localeCompare (#2141)
* chore: removes semver in favor of localeCompare

* chore: removes v
2023-04-21 13:52:38 -07:00
Amir Raminfar 12891b17ea chore(dev): adds https to localhost 2023-04-21 13:21:02 -07:00
Amir Raminfar 96b7189c44 chore: fix typecheck 2023-04-21 13:17:16 -07:00
Amir Raminfar 8489426540 chore: updates npm modules 2023-04-20 16:43:16 -07:00
renovate[bot] c592553db2 chore(deps): update dependency vite to v4.3.1 (#2135)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-20 16:41:21 -07:00
renovate[bot] 15e4cc7bce chore(deps): update node.js to v20 (#2139)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-20 16:41:08 -07:00
renovate[bot] 023a3490dc chore(deps): update dependency vitepress to v1.0.0-alpha.73 2023-04-20 18:01:01 +00:00
Amir Raminfar ee920b119e chore: update pnpm 2023-04-19 10:13:20 -07:00
Amir Raminfar fbcd52bcdd chore: updates npm modules 2023-04-18 09:49:05 -07:00
renovate[bot] 8b3bc673d0 chore(deps): update docker/bake-action action to v3 (#2136)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-18 09:45:17 -07:00
renovate[bot] 25341e2b7c chore(deps): update pnpm to v8.3.0 2023-04-18 02:47:07 +00:00
renovate[bot] 5c8437436e fix(deps): update module github.com/docker/docker to v23.0.4+incompatible 2023-04-18 01:45:28 +00:00
renovate[bot] 879dbb5ad0 chore(deps): update cypress/included docker tag to v12.10.0 2023-04-17 23:37:11 +00:00
renovate[bot] 7b4d084cb2 chore(deps): update dependency vitepress to v1.0.0-alpha.72 2023-04-17 20:17:04 +00:00
renovate[bot] c93ecbfdae chore(deps): update dependency vitepress to v1.0.0-alpha.71 2023-04-17 04:46:23 +00:00
renovate[bot] d79fcc2774 chore(deps): update dependency vitepress to v1.0.0-alpha.70 2023-04-16 17:49:28 +00:00
renovate[bot] 98b5c106ec chore(deps): update dependency vitepress to v1.0.0-alpha.69 2023-04-16 02:49:21 +00:00
renovate[bot] c44610fd14 chore(deps): update dependency vitepress to v1.0.0-alpha.66 2023-04-15 15:37:25 +00:00
Amir Raminfar 24a34146f2 docs: fixes document error before mounting 2023-04-14 09:13:15 -07:00
Amir Raminfar af96a5b18d chore: updates npm modules 2023-04-14 09:09:11 -07:00
Amir Raminfar f98bdc054a Updates release to bake 2023-04-14 08:59:58 -07:00
Amir Raminfar d92d153175 chore(docs): adds caching 2023-04-13 12:01:58 -07:00
Amir Raminfar 82e201973b chore: remove .netlify dir 2023-04-13 10:16:33 -07:00
Amir Raminfar 52b1425a41 docs: adds netlify function to fetch pull count 2023-04-13 10:15:34 -07:00
Amir Raminfar 547c3917c5 chore: updates npm modules 2023-04-12 13:27:27 -07:00
Amir Raminfar 2800421e8d ci: use docker bake instead (#2119)
* Updates to bake instead

* Removes buildx from compose

* Cleans up build again

* Removes load

* More clean up

* Tries to use bake correctly

* ci: use bake plugin again
2023-04-12 13:14:15 -07:00
Amir Raminfar 57d1c290d9 chore: release v4.9.4 2023-04-12 09:55:24 -07:00
Amir Raminfar 650413d510 Merge pull request #2123 from amir20/escape-html
fix: correctly escape XML entities on client side should fix #2120
2023-04-12 09:54:41 -07:00
Amir Raminfar 0e677bdee7 fix: correctly escape XML entities on client side 2023-04-12 09:47:27 -07:00
renovate[bot] 4455cc15cc chore(deps): update pnpm to v8.2.0 2023-04-10 22:04:50 +00:00
Amir Raminfar bc48387749 chore(renovate): updates renovate to auto merge 2023-04-10 09:25:57 -07:00
Amir Raminfar 8c625a3113 fix: fixes healthcheck when containers are stopped 2023-04-09 13:47:03 -07:00
Amir Raminfar 9775df895b docs: updates healthcheck docs 2023-04-09 13:27:14 -07:00
Amir Raminfar 5fc2d29e4f chore: cleans up e2e package.json file 2023-04-09 13:17:03 -07:00
Amir Raminfar 60cfdd4a70 chore: release v4.9.3 2023-04-09 13:00:00 -07:00
Amir Raminfar ad52a14d42 chore: updates all other modules 2023-04-09 12:55:53 -07:00
Amir Raminfar 792f640e41 chore: updates unocss 2023-04-09 12:55:21 -07:00
Amir Raminfar eb47420444 chore: uses npx changelogithub going forward with conventional commits 2023-04-09 12:49:04 -07:00
renovate[bot] 8492d73714 Update dependency @pinia/testing to ^0.0.16 (#2111) 2023-04-08 06:53:48 -07:00
Amir Raminfar c8565e08c9 Release 4.9.2 2023-04-06 09:53:52 -07:00
Amir Raminfar 63a0d544e4 Adds tooltip to healthcheck. Fixes #2104 (#2110) 2023-04-06 16:50:44 +00:00
renovate[bot] e7591f9a64 Update module github.com/docker/docker to v23.0.3+incompatible [SECURITY] (#2109) 2023-04-04 19:12:37 -07:00
renovate[bot] f242776772 Update golang Docker tag to v1.20.3 (#2108)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-04 13:26:28 -07:00
Amir Raminfar 51008ba604 Updates modules 2023-04-04 13:25:41 -07:00
renovate[bot] 96c68686bf Update pnpm to v8.1.1 (#2106)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-04 13:19:26 -07:00
renovate[bot] 93ddbca73a Update dependency vitepress to v1.0.0-alpha.65 (#2107)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-04 13:19:18 -07:00
Amir Raminfar a30de127ca Fixes spaces 2023-04-04 11:32:58 -07:00
Amir Raminfar 8e5611bc50 Updates docs to have analytics and dashboard 2023-04-03 12:00:15 -07:00
Amir Raminfar 9e2d6a484a Tries to remove unneeded npmrc changes 2023-03-31 08:32:02 -07:00
Amir Raminfar 7be7b17cc1 Uses corepack (#2103)
* Uses corepack

* Adds corepack to Docker
2023-03-30 10:15:18 -07:00
Amir Raminfar a619149412 Adds strict-peer-dependencies 2023-03-29 14:18:56 -07:00
Amir Raminfar 488156df89 Revert netlify 2023-03-29 14:12:54 -07:00
Amir Raminfar 1c068cc6f9 Temprorary fix for pnpm 2023-03-29 14:09:26 -07:00
renovate[bot] c6f2f8ccb2 Update dependency vitepress to v1.0.0-alpha.64 (#2093)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-29 12:49:27 -07:00
Amir Raminfar 518c78c3d1 Release 4.9.1 2023-03-29 12:38:13 -07:00
Amir Raminfar fe034d1a38 Removes -- 2023-03-29 12:29:26 -07:00
Amir Raminfar 49e6cd1c9b Changes to 1.20 (#2102) 2023-03-29 12:27:35 -07:00
Amir Raminfar 3e78f81a53 Release 4.9.0 2023-03-29 11:56:00 -07:00
Amir Raminfar e84f0cdb97 Adds typechecking with typescript (#2101)
* Fixes all other vue errors

* Adds typescheck

* Fix pnpm

* Fix pnpm again for cache
2023-03-29 11:53:15 -07:00
Amir Raminfar 804bd5b9e1 Fixes mobile bug 2023-03-29 09:59:21 -07:00
Amir Raminfar 9c9f247e0c cleans up typescript errors 2023-03-29 09:38:02 -07:00
Amir Raminfar db43f7a62d Fixes vue-tsc errors 2023-03-29 09:27:35 -07:00
Amir Raminfar efef1dca6b Fixes component d file 2023-03-29 09:20:47 -07:00
Amir Raminfar 3417ec3e99 Cleans up icons 2023-03-29 09:16:38 -07:00
Amir Raminfar bda832a8a3 Updates modules 2023-03-29 09:01:36 -07:00
renovate[bot] 30e9ab68d7 Update cypress/included Docker tag to v12.9.0 (#2100)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-29 08:59:02 -07:00
Amir Raminfar 9ba1a0f76a Adds health status by parsing the label from status (#2099)
* Adds healthcheck to status in menu

* Fixes bug with crated

* Moves colors to vars

* Updates icons
2023-03-29 08:58:12 -07:00
Amir Raminfar 0521f343e3 Also checks for level between [] 2023-03-28 16:04:56 -07:00
renovate[bot] 9ecc21dcb2 Update module github.com/docker/docker to v23.0.2+incompatible (#2098)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-28 09:55:33 -07:00
Amir Raminfar d99f4b2766 Updates pnpm 2023-03-28 09:08:38 -07:00
Amir Raminfar 5c83e33ace Updates circles 2023-03-27 19:01:25 -07:00
Amir Raminfar a4ac2c60ac Updates modules 2023-03-27 15:29:13 -07:00
Amir Raminfar bbb7fb8ba9 Updates modules 2023-03-26 09:01:49 -07:00
renovate[bot] 0ebd399768 Update dependency vitepress to v1.0.0-alpha.62 (#2092) 2023-03-25 08:40:57 -07:00
Amir Raminfar 9971b41434 Updates modules 2023-03-24 14:33:31 -07:00
Amir Raminfar 7f4d005a67 Update README.md 2023-03-24 13:43:45 -07:00
Amir Raminfar 2709417ba3 Release 4.8.2 2023-03-23 16:12:38 -07:00
Amir Raminfar c4386d2d84 Updates modules 2023-03-23 12:55:43 -07:00
Amir Raminfar fbcf989885 Updates with dark and light video 2023-03-21 15:25:03 -07:00
Amir Raminfar 07352f6e16 Adds dark and light for browser 2023-03-21 12:41:22 -07:00
Amir Raminfar 6fecfce3c7 Updates color on video 2023-03-21 12:35:32 -07:00
Amir Raminfar 2884ffafeb Adds blur 2023-03-21 12:33:06 -07:00
Amir Raminfar 67ac202b04 Updates videos 2023-03-20 13:08:18 -07:00
Amir Raminfar 839afd8fcf Fixes bug for level 2023-03-20 13:06:31 -07:00
Amir Raminfar 202b07bfb4 Updates logic to find ERROR in logs 2023-03-20 11:13:53 -07:00
renovate[bot] 15bd0999a2 Update dependency vite to v4.2.1 (#2088) 2023-03-20 06:08:57 -07:00
Amir Raminfar 4450c2527f node latest (#2089)
* Fixes tests

* Fixes tests

* Updates tests

* Updates node to latest

* Updates modules
2023-03-16 13:48:42 -07:00
renovate[bot] 614e62b782 Update cypress/included Docker tag to v12.8.1 (#2086) 2023-03-16 13:03:49 -07:00
Amir Raminfar 77cce86e29 Fixes file formatting (#2085)
* Updates prettier ignore

* Updates prettier ignore

* Updates formatting

* Fixes tests

* Fixes tests
2023-03-15 10:46:32 -07:00
renovate[bot] 93dc547b9d Update actions/setup-go action to v4 (#2084)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-15 09:14:43 -07:00
renovate[bot] 0681a3c0c6 Update cypress/included Docker tag to v12.8.0 (#2063)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-15 08:50:58 -07:00
renovate[bot] 9bf35e3e1f Update dependency vitepress to v1.0.0-alpha.60 (#2082)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-15 08:50:49 -07:00
Amir Raminfar 087e8694d1 Uses node lts instead (#2083)
* Uses node lts instead

* Uses node 18
2023-03-15 08:39:41 -07:00
renovate[bot] 34b034c822 Update dependency vitepress to v1.0.0-alpha.58 (#2080)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-14 14:43:29 -07:00
Amir Raminfar bd598699e3 Adds docker social link 2023-03-13 13:55:51 -07:00
Amir Raminfar 8867b2aa7c Updates docs 2023-03-13 09:51:53 -07:00
Amir Raminfar 296e8fc50e Fixes docker build 2023-03-13 09:19:35 -07:00
Amir Raminfar da065a1966 Fixes space 2023-03-13 09:15:41 -07:00
Amir Raminfar 5262a54463 Removes husky 2023-03-13 09:12:29 -07:00
Amir Raminfar f37c5d09ed Updates lint stage 2023-03-13 09:09:26 -07:00
Amir Raminfar 298e030db0 Uses simple-git-hooks 2023-03-13 09:08:09 -07:00
Amir Raminfar 6f2ef4499b Updates deps 2023-03-13 08:43:06 -07:00
Amir Raminfar e4d1064843 Updates npmrc 2023-03-13 08:30:24 -07:00
Amir Raminfar 0c1ad7c0c7 Adds include-workspace-root 2023-03-12 13:26:40 -07:00
Amir Raminfar fb9333a11b Fixes workspaces and int tests 2023-03-12 13:23:21 -07:00
Amir Raminfar ed67d7d085 Updates libs for dozzle 2023-03-12 12:46:14 -07:00
Amir Raminfar d34136589b Revert "Update dependency vitepress to v1.0.0-alpha.52 (#2078)" (#2079)
This reverts commit ac7b120176.
2023-03-11 16:45:22 -08:00
Amir Raminfar 5226c92f78 Adds sitemap 2023-03-11 15:18:22 -08:00
Amir Raminfar 9e507b31ec Fixes max width for video 2023-03-11 15:11:38 -08:00
renovate[bot] ac7b120176 Update dependency vitepress to v1.0.0-alpha.52 (#2078)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-11 15:06:31 -08:00
Amir Raminfar c15a680797 Adds more docs 2023-03-10 16:05:48 -08:00
Amir Raminfar 1c07142bce Adds workspace 2023-03-10 14:09:57 -08:00
renovate[bot] c886071559 Update docker/setup-buildx-action action to v2.5.0 (#2075)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-10 12:23:11 -08:00
renovate[bot] 44b6809abe Update dependency vitepress to v1.0.0-alpha.51 (#2077)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-10 12:23:01 -08:00
Amir Raminfar 13db2cfb58 Fixes style on mobile 2023-03-10 12:19:56 -08:00
Amir Raminfar a1af2c60cd vitepress (#2076)
* Uses vitepress

* migrates to vitepress

* Remove api link

* Adds gitignore

* Adds gitignore

* Removes unused colors

* Removes optional for docker
2023-03-10 12:01:26 -08:00
Amir Raminfar 37d322bec8 Adds gtag 2023-03-08 13:22:57 -08:00
Amir Raminfar ccd61ce095 Fixes js warn 2023-03-08 13:06:04 -08:00
Amir Raminfar 01c63a3650 Updates colors and adds button 2023-03-08 13:03:36 -08:00
Amir Raminfar 5a99378845 Updates dockerfile for int test 2023-03-08 10:34:08 -08:00
renovate[bot] dfed83df0b Update pnpm to v7.29.1 (#2068)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-08 10:22:00 -08:00
renovate[bot] 75b0d5c4fb Update module github.com/spf13/afero to v1.9.5 (#2071)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-08 10:21:41 -08:00
renovate[bot] d654f3eceb Update golang Docker tag to v1.20.2 (#2072)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-08 10:21:30 -08:00
renovate[bot] 6cdcbd8244 Update dependency @intlify/unplugin-vue-i18n to ^0.9.0 (#2073)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-08 10:21:19 -08:00
Amir Raminfar cb18e42602 Fixes pre 2023-03-06 11:45:13 -08:00
Amir Raminfar 37d78117f1 Fixes menu 2023-03-06 09:58:25 -08:00
Amir Raminfar 0cf4e6d2b6 Creates header 2023-03-06 09:45:32 -08:00
Amir Raminfar ec5196ea6e Removes comma 2023-03-06 09:31:37 -08:00
Amir Raminfar 089c2e9b17 Finishes menu 2023-03-06 09:27:33 -08:00
Amir Raminfar 5f2420960b Makes improves to colors and menu 2023-03-05 14:24:43 -08:00
Amir Raminfar e73ee6735e Adds guide link 2023-03-04 11:17:50 -08:00
Amir Raminfar 03b4c5adb9 Updates link colors 2023-03-03 15:20:11 -08:00
Amir Raminfar b92703c7bb Updates title and adds more content 2023-03-03 13:12:18 -08:00
Amir Raminfar 2bb4d10b66 Adds docs layouts 2023-03-03 09:50:01 -08:00
Amir Raminfar e02b08c21d Removes unused components 2023-03-02 15:57:12 -08:00
Amir Raminfar 539ace566c Adds icons for github and docker 2023-03-01 12:15:04 -08:00
Amir Raminfar a29373d1a2 Adds poster fix 2023-02-28 16:48:42 -08:00
Amir Raminfar 3c0eac4541 Adds poster 2023-02-28 16:32:10 -08:00
Amir Raminfar beee7ea9f8 Fixes grid bug 2023-02-28 15:51:08 -08:00
Amir Raminfar 770416805e Adds color to dozzle 2023-02-28 15:48:06 -08:00
Amir Raminfar c2fcecaec0 Updates modules 2023-02-28 13:45:25 -08:00
Amir Raminfar 3a30bcf937 Fixes grid 2023-02-28 13:42:39 -08:00
Amir Raminfar 54f9a04b9d Fixes grid problem 2023-02-28 13:10:24 -08:00
Amir Raminfar 62b7c38f88 Adds dockerignore 2023-02-28 12:02:31 -08:00
Amir Raminfar b474128a5f Fixes vscode settings 2023-02-28 12:00:07 -08:00
Amir Raminfar cb46130880 Moves netlify again 2023-02-28 11:51:34 -08:00
Amir Raminfar 8cf9fbe77c Moves netlify 2023-02-28 11:48:43 -08:00
Amir Raminfar 9c5da96eb4 Moves netlify 2023-02-28 11:48:43 -08:00
Amir Raminfar 5abf538cb4 Updates docs 2023-02-28 11:48:43 -08:00
renovate[bot] a1d79d4fc6 Update dependency vitest to ^0.29.0 (#2064)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-27 10:56:07 -08:00
renovate[bot] 468c8982b0 Update module github.com/stretchr/testify to v1.8.2 (#2065)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-27 10:55:51 -08:00
Amir Raminfar a9b4be43c8 Release 4.8.1 2023-02-26 08:41:39 -08:00
Amir Raminfar 1350895676 Updates logic when no TLS certs are found to use plan HTTP. See #2066 (#2067) 2023-02-25 12:57:35 -08:00
Amir Raminfar 591bee332f Release 4.8.0 2023-02-24 09:51:21 -08:00
Amir Raminfar 3257f4b04f Updates remadme file 2023-02-24 09:46:21 -08:00
Amir Raminfar 872729a93b Adds support for multiple hosts (#2059)
* Adds support for multiple hosts

* Adds UI for drop down

* Adds support for TLS and remove SSH

* Changes dropdown to only show up with >1 hosts

* Fixes js tests

* Fixes go tests

* Fixes download link

* Updates readme

* Removes unused imports

* Fixes spaces
2023-02-24 09:42:58 -08:00
renovate[bot] a7d6a5088a Update dependency vite to v4.1.4 (#2060)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-23 15:08:26 -08:00
renovate[bot] ebf832b501 Update module github.com/spf13/afero to v1.9.4 (#2061)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-23 15:08:14 -08:00
renovate[bot] 11fd3e14bb Update dependency unplugin-auto-import to ^0.15.0 (#2062)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-23 15:07:32 -08:00
Amir Raminfar eb4df34d42 Updates go modules 2023-02-21 08:17:49 -08:00
Amir Raminfar 6bc6dd5c03 Updates modules 2023-02-21 08:16:52 -08:00
renovate[bot] 460f1167b0 Update dependency vite to v4.1.3 (#2057) 2023-02-20 07:08:27 -08:00
renovate[bot] f04563e868 Update dependency @pinia/testing to ^0.0.15 (#2056) 2023-02-20 07:07:59 -08:00
renovate[bot] c4881cf407 Update cypress/included Docker tag to v12.6.0 (#2048) 2023-02-19 07:05:17 -08:00
renovate[bot] 9945f31e7c Update dependency vite to v4.1.2 (#2052) 2023-02-19 07:05:10 -08:00
renovate[bot] c13fd882ed Update dependency vite-plugin-vue-layouts to ^0.8.0 (#2053) 2023-02-19 06:16:37 -08:00
Amir Raminfar da6257c233 Updates modules 2023-02-16 13:24:36 -08:00
Amir Raminfar 0b5da03ffb Release 4.7.5 2023-02-15 12:25:48 -08:00
renovate[bot] 617fc3d387 Update golang Docker tag to v1.20.1 (#2046)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-15 12:24:20 -08:00
Amir Raminfar d82f9e4db2 Updates modules 2023-02-14 14:54:29 -08:00
renovate[bot] efabc926cf Update module github.com/docker/docker to v23.0.1+incompatible (#2043) 2023-02-14 06:17:35 -08:00
renovate[bot] c5505e9ac5 Update dependency unplugin-auto-import to ^0.14.0 (#2040)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-09 10:30:21 -08:00
Amir Raminfar bb14f9121e Properly handles casting of string. Fixes #2041 (#2042) 2023-02-09 09:46:46 -08:00
Amir Raminfar 050666a39c Uses 127.0.0.1 instead 2023-02-08 10:24:05 -08:00
renovate[bot] a03d83676f Update docker/setup-buildx-action action to v2.4.1 (#2035) 2023-02-06 06:30:41 -08:00
Amir Raminfar 6d4bff0ce8 Adds coverage to ignore 2023-02-03 08:58:34 -08:00
renovate[bot] 5e1b532f52 Update cypress/included Docker tag to v12.5.1 (#2034)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-03 08:38:28 -08:00
Amir Raminfar 4ea19f97da Adds more tests 2023-02-02 12:23:46 -08:00
Amir Raminfar 3420eb968e Updates go modules 2023-02-02 12:08:08 -08:00
Amir Raminfar f9f48c3f81 Updates npm modules and tests 2023-02-02 12:06:18 -08:00
renovate[bot] 5fad7b1efc Update dependency vite to v4.1.1 (#2033) 2023-02-02 07:09:46 -08:00
renovate[bot] 1337ad97f1 Update golang Docker tag to v1.20.0 (#2030) 2023-02-02 07:01:36 -08:00
renovate[bot] 520d6c05b1 Update dependency vite to v4.1.0 (#2031) 2023-02-02 07:01:27 -08:00
renovate[bot] 52bd643b63 Update cypress/included Docker tag to v12.5.0 (#2018) 2023-01-31 16:42:19 -08:00
Amir Raminfar 928136cb62 Release 4.7.4 2023-01-31 12:07:24 -08:00
Amir Raminfar 4c5bd9a7be Reverts back previous fix and uses proper error handeling (#2029)
* Reverts back previous fix and uses proper error handeling

* Adds more tests and refactors existing tests
2023-01-31 12:03:41 -08:00
renovate[bot] 9378bb5f51 Update docker/build-push-action action to v4 (#2026) 2023-01-31 05:27:45 -08:00
Amir Raminfar c036debd17 Release 4.7.3 2023-01-30 12:51:51 -08:00
Amir Raminfar d330ca4746 Fixes error when paginating (#2027) 2023-01-30 12:51:39 -08:00
Amir Raminfar 645b9a8311 Release 4.7.2 2023-01-30 11:17:29 -08:00
Amir Raminfar 862bd449fd Fixes int tests 2023-01-30 11:14:18 -08:00
Amir Raminfar 0a7ea0251a Uses docker compose instead docker-compose 2023-01-30 10:09:09 -08:00
renovate[bot] 5809a5478b Update docker/setup-buildx-action action to v2.4.0 (#2023)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-30 09:26:20 -08:00
Amir Raminfar 8bbdfb4a9e Uses gha cache 2023-01-30 09:16:57 -08:00
Amir Raminfar 74b5ff48fc Updates cypress 2023-01-30 09:16:39 -08:00
Amir Raminfar c6c24aaf9f Changes minimum ms to 10ms 2023-01-30 09:00:53 -08:00
Amir Raminfar e133b59969 Updates tests 2023-01-30 08:54:46 -08:00
Amir Raminfar 61fea7afaa Hide date when mobile 2023-01-29 13:39:53 -08:00
renovate[bot] 46aa4ab005 Update dependency unplugin-auto-import to ^0.13.0 (#2022) 2023-01-29 13:18:38 -08:00
renovate[bot] 8a8b2607a1 Update docker/setup-buildx-action action to v2.3.0 (#2020)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-28 06:42:39 -08:00
renovate[bot] 5df604e480 Update dependency unplugin-vue-components to ^0.23.0 (#2021)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-28 06:42:31 -08:00
Amir Raminfar 7a169dd555 Release 4.7.1 2023-01-27 13:30:55 -08:00
Amir Raminfar af49e6fd6f Does not center multiline 2023-01-27 12:40:05 -08:00
Amir Raminfar 36a03d91fc Fixes error position to use flex instead (#2019) 2023-01-27 11:03:22 -08:00
Amir Raminfar 143246237d Fixes prefix bug 2023-01-25 13:02:33 -08:00
Amir Raminfar a0dbfeaba5 Release 4.7.0 2023-01-25 11:04:13 -08:00
Amir Raminfar d19d6efa8b Release 4.6.5 2023-01-25 10:54:08 -08:00
Amir Raminfar fd97c50da8 Fixes release 2023-01-25 10:54:00 -08:00
Amir Raminfar de109abd76 Release 4.6.4 2023-01-25 10:52:56 -08:00
Amir Raminfar 0e5830df20 Makes changes to log streamer to guess the event level (#2013)
* Makes changes to log streamer to guess the event level

* Updates diff

* Adds log class for level

* Groups messages by level

* Fixes bugs for grouping

* Fixes tests

* Fixes json bug

* Updates logic to support other kind of levels

* Fixes mobile view
2023-01-25 10:39:21 -08:00
Amir Raminfar 20e158c7bd Uses latest 2023-01-25 10:26:10 -08:00
Amir Raminfar b77c908719 Updates node 2023-01-25 10:24:08 -08:00
Amir Raminfar 5f9dc2780c Adds -- 2023-01-25 10:09:02 -08:00
Amir Raminfar 9c905da702 Adds --outputTruncateLength=500 2023-01-25 10:07:26 -08:00
Amir Raminfar 4729e7f29e Fixes tests 2023-01-25 09:50:07 -08:00
renovate[bot] 10c5a2739c Update dependency vitest to ^0.28.0 (#2016)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-23 08:51:39 -08:00
renovate[bot] 8d75729bca Update module github.com/docker/docker to v20.10.23+incompatible (#2014)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-23 08:51:29 -08:00
Amir Raminfar 3f61b8cfdc Release 4.6.3 2023-01-19 11:37:53 -08:00
Amir Raminfar 3a55fdfff0 Updates modules 2023-01-19 11:37:46 -08:00
renovate[bot] 941fb60336 Update module github.com/dustin/go-humanize to v1.0.1 (#2010)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-17 11:46:46 -08:00
renovate[bot] 133261fba2 Update docker/build-push-action action to v3.3.0 (#2011) 2023-01-14 08:06:59 -08:00
renovate[bot] 9fc01567a2 Update golang Docker tag to v1.19.5 (#2009)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-11 10:03:11 -08:00
renovate[bot] 4118204948 Update dependency vitest to ^0.27.0 (#2007) 2023-01-09 07:11:38 -08:00
dependabot[bot] 12bb8d3f68 Bump github.com/docker/distribution (#2004)
Bumps [github.com/docker/distribution](https://github.com/docker/distribution) from 2.7.1+incompatible to 2.8.0+incompatible.
- [Release notes](https://github.com/docker/distribution/releases)
- [Commits](https://github.com/docker/distribution/compare/v2.7.1...v2.8.0)

---
updated-dependencies:
- dependency-name: github.com/docker/distribution
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-07 13:39:14 -08:00
dependabot[bot] 88517ff0f7 Bump github.com/opencontainers/image-spec from 1.0.1 to 1.0.2 (#2005) 2023-01-07 12:25:33 -08:00
renovate[bot] 6dd3758f98 Update dependency jsdom to v21 (#2003) 2023-01-07 09:58:42 -08:00
Amir Raminfar e3630465ed Updates deps 2023-01-06 09:59:26 -08:00
renovate[bot] 4715f18c07 Update cypress/included Docker tag to v12.3.0 (#2002)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-06 09:46:19 -08:00
Amir Raminfar b1d4520360 Updates deps 2023-01-03 10:04:13 -08:00
renovate[bot] 467b5e8e19 Update dependency unplugin-icons to ^0.15.0 (#2001)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-03 10:03:27 -08:00
182 changed files with 9237 additions and 6562 deletions
+1 -1
View File
@@ -5,4 +5,4 @@ node_modules
dist
.git
e2e
docs
+8 -7
View File
@@ -1,10 +1,9 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
title: ""
labels: ""
assignees: amir20
---
**Describe the bug**
@@ -12,6 +11,7 @@ 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 '....'
@@ -24,9 +24,10 @@ A clear and concise description of what you expected to happen.
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`]
- 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**
+1 -2
View File
@@ -1,10 +1,9 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
title: ""
labels: enhancement
assignees: amir20
---
**Is your feature request related to a problem? Please describe.**
+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base", "schedule:weekly", "group:allNonMajor", ":automergeMinor"],
"rangeStrategy": "bump"
}
+53 -29
View File
@@ -8,40 +8,70 @@ jobs:
name: JavaScript Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install Node
uses: actions/setup-node@v3
- name: Install pnpm
uses: pnpm/action-setup@v2.2.4
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
name: Install Node
with:
version: 6.20.1
node-version: latest
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v3
with:
node-version: latest
cache: "pnpm"
cache-dependency-path: "**/pnpm-lock.yaml"
- name: Install dependencies
run: pnpm install
run: pnpm install --frozen-lockfile --prefer-offline
- name: Run Tests
run: pnpm run test
run: pnpm run test -- --outputTruncateLength=500
go-test:
name: Go Tests
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v3
uses: actions/setup-go@v4
with:
go-version: 1.19.x
go-version: 1.20.x
- name: Checkout code
uses: actions/checkout@v3
- name: Run Go Tests with Coverage
run: make test SKIP_ASSET=1
int-test:
name: Integration Tests
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Build images
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml build
- name: Run tests
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml up --build --force-recreate --exit-code-from cypress
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
name: Install Node
with:
node-version: latest
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v3
with:
node-version: latest
cache: "pnpm"
cache-dependency-path: "**/pnpm-lock.yaml"
- name: Install dependencies
run: pnpm install
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build
uses: docker/bake-action@v3
with:
load: true
set: |
*.cache-from=type=gha
*.cache-to=type=gha,mode=max
- name: Run Playwright tests
run: docker compose up --exit-code-from playwright
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
buildx:
needs: [go-test, npm-test, int-test]
name: Release
@@ -53,22 +83,22 @@ jobs:
with:
images: amir20/dozzle
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.2.1
uses: docker/setup-buildx-action@v2.9.1
- name: Login to DockerHub
uses: docker/login-action@v2.1.0
uses: docker/login-action@v2.2.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v3.2.0
uses: docker/build-push-action@v4.1.1
with:
push: true
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }}
build-args: TAG=${{ steps.meta.outputs.version }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=amir20/dozzle:latest
cache-to: type=inline
cache-from: type=gha
cache-to: type=gha,mode=max
git-release:
needs: [buildx]
name: Github Release
@@ -80,13 +110,7 @@ jobs:
fetch-depth: 0
- name: Install Node
uses: actions/setup-node@v3
- name: Install pnpm
uses: pnpm/action-setup@v2.2.4
with:
version: 6.20.1
- name: Install dependencies
run: pnpm install
- name: Release to Github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: pnpm run release -- --github.release --no-increment --no-git --ci
run: npx changelogithub
+5 -5
View File
@@ -18,19 +18,19 @@ jobs:
with:
images: amir20/dozzle
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.2.1
uses: docker/setup-buildx-action@v2.9.1
- name: Login to DockerHub
uses: docker/login-action@v2.1.0
uses: docker/login-action@v2.2.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v3.2.0
uses: docker/build-push-action@v4.1.1
with:
push: true
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }}
build-args: TAG=${{ steps.meta.outputs.version }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=amir20/dozzle:master
cache-to: type=inline
cache-from: type=gha
cache-to: type=gha,mode=max
+64 -39
View File
@@ -7,66 +7,91 @@ on:
- master
name: Test
jobs:
typecheck:
name: Typecheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
name: Install Node
with:
node-version: latest
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v3
with:
node-version: latest
cache: "pnpm"
cache-dependency-path: "**/pnpm-lock.yaml"
- name: Install dependencies
run: pnpm install --frozen-lockfile --prefer-offline
- name: Run Typecheck
run: pnpm run typecheck
npm-test:
name: JavaScript Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install Node
uses: actions/setup-node@v3
- name: Install pnpm
uses: pnpm/action-setup@v2.2.4
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
name: Install Node
with:
version: 6.20.1
node-version: latest
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v3
with:
node-version: latest
cache: "pnpm"
cache-dependency-path: "**/pnpm-lock.yaml"
- name: Install dependencies
run: pnpm install
run: pnpm install --frozen-lockfile --prefer-offline
- name: Run Tests
run: pnpm run test
run: pnpm run test -- --outputTruncateLength=500
go-test:
name: Go Tests
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v3
uses: actions/setup-go@v4
with:
go-version: 1.19.x
go-version: 1.20.x
- name: Checkout code
uses: actions/checkout@v3
- name: Run Go Tests with Coverage
run: make test SKIP_ASSET=1
int-test:
name: Integration Tests
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
name: Install Node
with:
fetch-depth: 2
node-version: latest
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v3
with:
node-version: latest
cache: "pnpm"
cache-dependency-path: "**/pnpm-lock.yaml"
- name: Install dependencies
run: pnpm install
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.2.1
- name: Login to DockerHub
if: ${{ !github.event.repository.fork && !github.event.pull_request.head.repo.fork }}
uses: docker/login-action@v2.1.0
uses: docker/setup-buildx-action@v2
- name: Build
uses: docker/bake-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build images
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml build --build-arg BUILDKIT_INLINE_CACHE=1
- name: Push images
if: ${{ !github.event.repository.fork && !github.event.pull_request.head.repo.fork }}
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml push
- name: Set commit message for push
if: github.event_name == 'push'
run: |
echo "GIT_LOG_MESSAGE<<EOF" >> $GITHUB_ENV
git log -1 --pretty=%B ${GITHUB_SHA} >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Set commit message for pull request
if: github.event_name == 'pull_request'
run: |
echo "GIT_LOG_MESSAGE<<EOF" >> $GITHUB_ENV
git log -1 --pretty=%B ${{github.event.pull_request.head.sha}} >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Run tests
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml up --build --force-recreate --exit-code-from cypress
load: true
set: |
*.cache-from=type=gha
*.cache-to=type=gha,mode=max
- name: Run Playwright tests
run: docker compose up --exit-code-from playwright
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
+6 -2
View File
@@ -1,4 +1,4 @@
certs
dist
node_modules
.cache
@@ -6,4 +6,8 @@ static
dozzle
coverage
.pnpm-debug.log
.vscode
coverage.out
.netlify
/test-results/
/playwright-report/
/playwright/.cache/
-1
View File
@@ -1 +0,0 @@
_
-4
View File
@@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname $0)/_/husky.sh"
pnpm lint-staged
+3
View File
@@ -0,0 +1,3 @@
shared-workspace-lockfile=false
include-workspace-root=true
ignore-workspace-root-check=true
+5
View File
@@ -0,0 +1,5 @@
auto-imports.d.ts
components.d.ts
docs/.vitepress/cache
docs/.vitepress/dist
dist
+1 -1
View File
@@ -1 +1 @@
-r '\.(go)$' -R 'node_modules' -G '*_test.go' -s -- go run main.go --level debug
-r '\.(go)$' -R 'node_modules' -G '\*\_test.go' -s -- go run main.go --level debug --remote-host tcp://64.225.88.189:2376|clashleaders.com
+6
View File
@@ -0,0 +1,6 @@
{
"i18n-ally.localesPaths": ["locales"],
"i18n-ally.keystyle": "nested",
"cSpell.words": ["healthcheck"],
"editor.formatOnSave": true
}
+13 -13
View File
@@ -14,22 +14,22 @@ appearance, race, religion, or sexual identity and orientation.
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
+8 -8
View File
@@ -1,28 +1,28 @@
# Build assets
FROM --platform=$BUILDPLATFORM node:19-alpine as node
RUN npm install -g pnpm
FROM --platform=$BUILDPLATFORM node:20-alpine as node
RUN corepack enable
WORKDIR /build
# Install dependencies from lock file
COPY pnpm-lock.yaml ./
RUN pnpm fetch
COPY pnpm-*.yaml ./
RUN pnpm fetch --ignore-scripts --no-optional
# Copy package.json and install dependencies
COPY package.json ./
RUN pnpm install -r --offline --ignore-scripts
RUN pnpm install --offline --ignore-scripts --no-optional
# Copy assets and translations to build
COPY .* vite.config.ts index.html ./
COPY .* vite.config.ts ./
COPY assets ./assets
COPY locales ./locales
COPY public ./public
# Build assets
RUN pnpm build
FROM --platform=$BUILDPLATFORM golang:1.19.4-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.21.0-alpine AS builder
RUN apk add --no-cache ca-certificates && mkdir /dozzle
+1 -1
View File
@@ -31,4 +31,4 @@ dev:
.PHONY: int
int:
docker-compose -f e2e/docker-compose.yml up --build --force-recreate --exit-code-from cypress
docker compose up --build --force-recreate --exit-code-from playwright
+32 -12
View File
@@ -2,7 +2,7 @@
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.
![Image](https://github.com/amir20/dozzle/blob/master/.github/demo.gif?raw=true)
https://user-images.githubusercontent.com/260667/227634771-9ebbe381-16a8-465a-b28a-450c5cd20c94.mp4
[![Go Report Card](https://goreportcard.com/badge/github.com/amir20/dozzle)](https://goreportcard.com/report/github.com/amir20/dozzle)
[![Docker Pulls](https://img.shields.io/docker/pulls/amir20/dozzle.svg)](https://hub.docker.com/r/amir20/dozzle/)
@@ -37,16 +37,7 @@ The simplest way to use dozzle is to run the docker container. Also, mount the D
Dozzle will be available at [http://localhost:8888/](http://localhost:8888/). You can change `-p 8888:8080` to any port. For example, if you want to view dozzle over port 4040 then you would do `-p 4040:8080`.
### With Docker swarm
docker service create \
--name=dozzle \
--publish=8888:8080 \
--constraint=node.role==manager \
--mount=type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
amir20/dozzle:latest
### With Docker compose
### Connecting with Docker compose
version: "3"
services:
@@ -58,7 +49,35 @@ Dozzle will be available at [http://localhost:8888/](http://localhost:8888/). Yo
ports:
- 9999:8080
### Enabling health check
### Connecting to remote hosts
Dozzle supports connecting to multiple remote hosts via `tcp://` using TLS or without. Appropriate certs need to be mounted for Dozzle to be able to successfully connect. At this point, `ssh://` is not supported because Dozzle docker image does not ship with any ssh clients.
To configure remote hosts, `--remote-host` or `DOZZLE_REMOTE_HOST` need to provided and the `pem` files need to be mounted to `/cert` directory. The `/cert` directory expects to have `/certs/{ca,cert,key}.pem` or `/certs/{host}/{ca,cert,key}.pem` in case of multiple hosts.
Below are examples of using `--remote-host` via CLI:
$ docker run -v /var/run/docker.sock:/var/run/docker.sock -v /path/to/certs:/certs -p 8080:8080 amir20/dozzle --remote-host tcp://167.99.1.1:2376
Multiple `--remote-host` flags can be used to specify multiple hosts.
Or to use compose:
version: "3"
services:
dozzle:
image: amir20/dozzle:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /path/to/certs:/certs
ports:
- 8080:8080
environment:
DOZZLE_REMOTE_HOST: tcp://167.99.1.1:2376,tcp://167.99.1.2:2376
You need to make sure appropriate certs are provided in `/certs/167.99.1.1/{ca,cert,key}.pem` and `/certs/167.99.1.2/{ca,cert,key}.pem` for both hosts to work.
### Adding health check
Dozzle doesn't enable healthcheck by default as it adds extra CPU usage. `healthcheck` can be enabled manually.
@@ -129,6 +148,7 @@ Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can
| `--usernamefile` | `DOZZLE_USERNAME_FILE` | `""` |
| `--passwordfile` | `DOZZLE_PASSWORD_FILE` | `""` |
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
| `--remote-host` | `DOZZLE_REMOTE_HOST` | |
## Troubleshooting and FAQs
+10 -9
View File
@@ -1,17 +1,18 @@
package analytics
type StartEvent struct {
ClientId string `json:"-"`
Version string `json:"version"`
FilterLength int `json:"filterLength"`
CustomAddress bool `json:"customAddress"`
CustomBase bool `json:"customBase"`
Protected bool `json:"protected"`
HasHostname bool `json:"hasHostname"`
ClientId string `json:"-"`
Version string `json:"version"`
FilterLength int `json:"filterLength"`
RemoteHostLength int `json:"remoteHostLength"`
CustomAddress bool `json:"customAddress"`
CustomBase bool `json:"customBase"`
Protected bool `json:"protected"`
HasHostname bool `json:"hasHostname"`
}
type RequestEvent struct {
ClientId string `json:"-"`
TotalContainers int `json:"totalContainers"`
ClientId string `json:"-"`
TotalContainers int `json:"totalContainers"`
RunningContainers int `json:"runningContainers"`
}
+389 -6
View File
@@ -1,8 +1,12 @@
// Generated by 'unplugin-auto-import'
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
const $$: typeof import('vue/macros')['$$']
const $: typeof import('vue/macros')['$']
const $$: typeof import('vue/macros')['$$']
const $computed: typeof import('vue/macros')['$computed']
const $customRef: typeof import('vue/macros')['$customRef']
const $ref: typeof import('vue/macros')['$ref']
@@ -29,7 +33,9 @@ declare global {
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createPinia: typeof import('pinia')['createPinia']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
@@ -46,6 +52,7 @@ declare global {
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const getDeep: typeof import('./utils/index')['getDeep']
const globalShowPopup: typeof import('./composables/popup')['globalShowPopup']
const h: typeof import('vue')['h']
const hourStyle: typeof import('./composables/settings')['hourStyle']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
@@ -53,6 +60,7 @@ declare global {
const isDefined: typeof import('@vueuse/core')['isDefined']
const isMobile: typeof import('./composables/media')['isMobile']
const isObject: typeof import('./utils/index')['isObject']
const isPinnedContainer: typeof import('./composables/storage')['isPinnedContainer']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
@@ -87,7 +95,8 @@ 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('./utils/index')['persistentVisibleKeys']
const persistentVisibleKeys: typeof import('./composables/storage')['persistentVisibleKeys']
const pinnedContainers: typeof import('./composables/storage')['pinnedContainers']
const provide: typeof import('vue')['provide']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
@@ -103,10 +112,10 @@ declare global {
const refThrottled: typeof import('@vueuse/core')['refThrottled']
const refWithControl: typeof import('@vueuse/core')['refWithControl']
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveDirective: typeof import('vue')['resolveDirective']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const search: typeof import('./composables/settings')['search']
const sessionHost: typeof import('./composables/storage')['sessionHost']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const setTitle: typeof import('./composables/title')['setTitle']
@@ -115,6 +124,7 @@ declare global {
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const showAllContainers: typeof import('./composables/settings')['showAllContainers']
const showStd: typeof import('./composables/settings')['showStd']
const showTimestamp: typeof import('./composables/settings')['showTimestamp']
const size: typeof import('./composables/settings')['size']
const smallerScrollbars: typeof import('./composables/settings')['smallerScrollbars']
@@ -130,6 +140,8 @@ declare global {
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const togglePinnedContainer: typeof import('./composables/storage')['togglePinnedContainer']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
@@ -140,14 +152,19 @@ declare global {
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
const useArraySome: typeof import('@vueuse/core')['useArraySome']
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
@@ -190,6 +207,7 @@ declare global {
const useEventBus: typeof import('@vueuse/core')['useEventBus']
const useEventListener: typeof import('@vueuse/core')['useEventListener']
const useEventSource: typeof import('@vueuse/core')['useEventSource']
const useExponentialMovingAverage: typeof import('./utils/index')['useExponentialMovingAverage']
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
const useFavicon: typeof import('@vueuse/core')['useFavicon']
const useFetch: typeof import('@vueuse/core')['useFetch']
@@ -233,14 +251,18 @@ declare global {
const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useParallax: typeof import('@vueuse/core')['useParallax']
const useParentElement: typeof import('@vueuse/core')['useParentElement']
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
@@ -252,6 +274,7 @@ declare global {
const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSearchFilter: typeof import('./composables/search')['useSearchFilter']
const useSeoMeta: typeof import('@vueuse/head')['useSeoMeta']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useSlots: typeof import('vue')['useSlots']
@@ -300,8 +323,10 @@ declare global {
const watchArray: typeof import('@vueuse/core')['watchArray']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
const watchDeep: typeof import('@vueuse/core')['watchDeep']
const watchEffect: typeof import('vue')['watchEffect']
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchPausable: typeof import('@vueuse/core')['watchPausable']
const watchPostEffect: typeof import('vue')['watchPostEffect']
@@ -311,6 +336,11 @@ declare global {
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: typeof import('@vueuse/core')['whenever']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue'
}
// for vue template auto import
import { UnwrapRef } from 'vue'
declare module 'vue' {
@@ -343,7 +373,9 @@ declare module 'vue' {
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
@@ -360,6 +392,7 @@ declare module 'vue' {
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getDeep: UnwrapRef<typeof import('./utils/index')['getDeep']>
readonly globalShowPopup: UnwrapRef<typeof import('./composables/popup')['globalShowPopup']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly hourStyle: UnwrapRef<typeof import('./composables/settings')['hourStyle']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
@@ -401,7 +434,8 @@ 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('./utils/index')['persistentVisibleKeys']>
readonly persistentVisibleKeys: UnwrapRef<typeof import('./composables/storage')['persistentVisibleKeys']>
readonly pinnedContainers: UnwrapRef<typeof import('./composables/storage')['pinnedContainers']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
@@ -417,10 +451,10 @@ declare module 'vue' {
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveDirective: UnwrapRef<typeof import('vue')['resolveDirective']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly search: UnwrapRef<typeof import('./composables/settings')['search']>
readonly sessionHost: UnwrapRef<typeof import('./composables/storage')['sessionHost']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly setTitle: UnwrapRef<typeof import('./composables/title')['setTitle']>
@@ -429,6 +463,7 @@ declare module 'vue' {
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly showAllContainers: UnwrapRef<typeof import('./composables/settings')['showAllContainers']>
readonly showStd: UnwrapRef<typeof import('./composables/settings')['showStd']>
readonly showTimestamp: UnwrapRef<typeof import('./composables/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./composables/settings')['size']>
readonly smallerScrollbars: UnwrapRef<typeof import('./composables/settings')['smallerScrollbars']>
@@ -444,6 +479,8 @@ declare module 'vue' {
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly togglePinnedContainer: UnwrapRef<typeof import('./composables/storage')['togglePinnedContainer']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
@@ -454,14 +491,19 @@ declare module 'vue' {
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
@@ -504,6 +546,7 @@ declare module 'vue' {
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
readonly useExponentialMovingAverage: UnwrapRef<typeof import('./utils/index')['useExponentialMovingAverage']>
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
@@ -547,14 +590,18 @@ declare module 'vue' {
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
@@ -566,6 +613,7 @@ declare module 'vue' {
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSearchFilter: UnwrapRef<typeof import('./composables/search')['useSearchFilter']>
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
@@ -614,8 +662,343 @@ declare module 'vue' {
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']>
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
}
}
declare module '@vue/runtime-core' {
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('./composables/settings')['DEFAULT_SETTINGS']>
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly collapseNav: UnwrapRef<typeof import('./composables/settings')['collapseNav']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
readonly config: UnwrapRef<typeof import('./stores/config')['default']>
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly flattenJSON: UnwrapRef<typeof import('./utils/index')['flattenJSON']>
readonly formatBytes: UnwrapRef<typeof import('./utils/index')['formatBytes']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getDeep: UnwrapRef<typeof import('./utils/index')['getDeep']>
readonly globalShowPopup: UnwrapRef<typeof import('./composables/popup')['globalShowPopup']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly hourStyle: UnwrapRef<typeof import('./composables/settings')['hourStyle']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isMobile: UnwrapRef<typeof import('./composables/media')['isMobile']>
readonly isObject: UnwrapRef<typeof import('./utils/index')['isObject']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly lightTheme: UnwrapRef<typeof import('./composables/settings')['lightTheme']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly menuWidth: UnwrapRef<typeof import('./composables/settings')['menuWidth']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly persistentVisibleKeys: UnwrapRef<typeof import('./composables/storage')['persistentVisibleKeys']>
readonly pinnedContainers: UnwrapRef<typeof import('./composables/storage')['pinnedContainers']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']>
readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
readonly ref: UnwrapRef<typeof import('vue')['ref']>
readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly search: UnwrapRef<typeof import('./composables/settings')['search']>
readonly sessionHost: UnwrapRef<typeof import('./composables/storage')['sessionHost']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly setTitle: UnwrapRef<typeof import('./composables/title')['setTitle']>
readonly settings: UnwrapRef<typeof import('./composables/settings')['settings']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly showAllContainers: UnwrapRef<typeof import('./composables/settings')['showAllContainers']>
readonly showStd: UnwrapRef<typeof import('./composables/settings')['showStd']>
readonly showTimestamp: UnwrapRef<typeof import('./composables/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./composables/settings')['size']>
readonly smallerScrollbars: UnwrapRef<typeof import('./composables/settings')['smallerScrollbars']>
readonly softWrap: UnwrapRef<typeof import('./composables/settings')['softWrap']>
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
readonly stripVersion: UnwrapRef<typeof import('./utils/index')['stripVersion']>
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']>
readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly togglePinnedContainer: UnwrapRef<typeof import('./composables/storage')['togglePinnedContainer']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']>
readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']>
readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']>
readonly unref: UnwrapRef<typeof import('vue')['unref']>
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useContainerStore: UnwrapRef<typeof import('./stores/container')['useContainerStore']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']>
readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']>
readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']>
readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']>
readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']>
readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']>
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
readonly useExponentialMovingAverage: UnwrapRef<typeof import('./utils/index')['useExponentialMovingAverage']>
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']>
readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']>
readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useHead: UnwrapRef<typeof import('@vueuse/head')['useHead']>
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useLogStream: UnwrapRef<typeof import('./composables/eventsource')['useLogStream']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSearchFilter: UnwrapRef<typeof import('./composables/search')['useSearchFilter']>
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
readonly useVisibleFilter: UnwrapRef<typeof import('./composables/visible')['useVisibleFilter']>
readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>
readonly watch: UnwrapRef<typeof import('vue')['watch']>
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
+41 -21
View File
@@ -1,52 +1,72 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {}
declare module '@vue/runtime-core' {
declare module 'vue' {
export interface GlobalComponents {
CarbonCaretDown: typeof import('~icons/carbon/caret-down')['default']
CilColumns: typeof import('~icons/cil/columns')['default']
CilFindInPage: typeof import('~icons/cil/find-in-page')['default']
BarChart: typeof import('./components/BarChart.vue')['default']
'Carbon:caretDown': typeof import('~icons/carbon/caret-down')['default']
'Carbon:circleSolid': typeof import('~icons/carbon/circle-solid')['default']
'Carbon:macShift': typeof import('~icons/carbon/mac-shift')['default']
'Carbon:star': typeof import('~icons/carbon/star')['default']
'Carbon:starFilled': typeof import('~icons/carbon/star-filled')['default']
'Cil:checkCircle': typeof import('~icons/cil/check-circle')['default']
'Cil:circle': typeof import('~icons/cil/circle')['default']
'Cil:columns': typeof import('~icons/cil/columns')['default']
'Cil:findInPage': typeof import('~icons/cil/find-in-page')['default']
'Cil:xCircle': typeof import('~icons/cil/x-circle')['default']
ComplexLogItem: typeof import('./components/LogViewer/ComplexLogItem.vue')['default']
ContainerHealth: typeof import('./components/LogViewer/ContainerHealth.vue')['default']
ContainerPopup: typeof import('./components/LogViewer/ContainerPopup.vue')['default']
ContainerStat: typeof import('./components/LogViewer/ContainerStat.vue')['default']
ContainerTable: typeof import('./components/ContainerTable.vue')['default']
ContainerTitle: typeof import('./components/LogViewer/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']
DropdownMenu: typeof import('./components/DropdownMenu.vue')['default']
FieldList: typeof import('./components/LogViewer/FieldList.vue')['default']
FuzzySearchModal: typeof import('./components/FuzzySearchModal.vue')['default']
InfiniteLoader: typeof import('./components/InfiniteLoader.vue')['default']
KeyShortcut: typeof import('./components/KeyShortcut.vue')['default']
LogActionsToolbar: typeof import('./components/LogViewer/LogActionsToolbar.vue')['default']
LogContainer: typeof import('./components/LogViewer/LogContainer.vue')['default']
LogDate: typeof import('./components/LogViewer/LogDate.vue')['default']
LogEventSource: typeof import('./components/LogViewer/LogEventSource.vue')['default']
LogLevel: typeof import('./components/LogViewer/LogLevel.vue')['default']
LogStd: typeof import('./components/LogViewer/LogStd.vue')['default']
LogViewer: typeof import('./components/LogViewer/LogViewer.vue')['default']
LogViewerWithSource: typeof import('./components/LogViewer/LogViewerWithSource.vue')['default']
MdiDotsVertical: typeof import('~icons/mdi/dots-vertical')['default']
MdiLightChevronDoubleDown: typeof import('~icons/mdi-light/chevron-double-down')['default']
MdiLightChevronLeft: typeof import('~icons/mdi-light/chevron-left')['default']
MdiLightChevronRight: typeof import('~icons/mdi-light/chevron-right')['default']
MdiLightCog: typeof import('~icons/mdi-light/cog')['default']
MdiLightLogout: typeof import('~icons/mdi-light/logout')['default']
MdiLightMagnify: typeof import('~icons/mdi-light/magnify')['default']
MobileMenu: typeof import('./components/MobileMenu.vue')['default']
OcticonContainer24: typeof import('~icons/octicon/container24')['default']
OcticonDownload24: typeof import('~icons/octicon/download24')['default']
OcticonTrash24: typeof import('~icons/octicon/trash24')['default']
PastTime: typeof import('./components/PastTime.vue')['default']
RelativeTime: typeof import('./components/RelativeTime.vue')['default']
'Mdi:arrowUp': typeof import('~icons/mdi/arrow-up')['default']
'Mdi:dotsVertical': typeof import('~icons/mdi/dots-vertical')['default']
'Mdi:lightChevronDoubleDown': typeof import('~icons/mdi-light/chevron-double-down')['default']
'Mdi:lightChevronLeft': typeof import('~icons/mdi-light/chevron-left')['default']
'Mdi:lightChevronRight': typeof import('~icons/mdi-light/chevron-right')['default']
'Mdi:lightCog': typeof import('~icons/mdi-light/cog')['default']
'Mdi:lightLogout': typeof import('~icons/mdi-light/logout')['default']
'Mdi:lightMagnify': typeof import('~icons/mdi-light/magnify')['default']
MobileMenu: typeof import('./components/common/MobileMenu.vue')['default']
'Octicon:container24': typeof import('~icons/octicon/container24')['default']
'Octicon:download24': typeof import('~icons/octicon/download24')['default']
'Octicon:trash24': typeof import('~icons/octicon/trash24')['default']
'Ph:command': typeof import('~icons/ph/command')['default']
'Ph:controlBold': typeof import('~icons/ph/control-bold')['default']
Popup: typeof import('./components/Popup.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ScrollableView: typeof import('./components/ScrollableView.vue')['default']
ScrollProgress: typeof import('./components/ScrollProgress.vue')['default']
Search: typeof import('./components/Search.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']
StatMonitor: typeof import('./components/LogViewer/StatMonitor.vue')['default']
StatSparkline: typeof import('./components/LogViewer/StatSparkline.vue')['default']
Tag: typeof import('./components/common/Tag.vue')['default']
ZigZag: typeof import('./components/LogViewer/ZigZag.vue')['default']
}
}
+31
View File
@@ -0,0 +1,31 @@
<template>
<div class="is-relative">
<div class="bar"></div>
<div class="is-overlay">
<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 {
height: 1.5em;
background-color: var(--primary-color);
transform-origin: left;
transform: scaleX(v-bind(minValue));
transition: transform 0.2s ease-out;
border-top-right-radius: 0.2em;
border-bottom-right-radius: 0.2em;
}
.is-overlay {
font-size: 0.9em;
padding: 0 0.5em;
}
</style>
+156
View File
@@ -0,0 +1,156 @@
!
<template>
<table class="table is-fullwidth">
<thead>
<tr :data-direction="direction > 0 ? 'asc' : 'desc'">
<th
v-for="(value, key) in fields"
:key="key"
@click.prevent="sort(key)"
:class="{ 'selected-sort': key === sortField }"
v-show="isVisible(key)"
>
<a>
<span class="icon-text">
<span>{{ $t(value.label) }}</span>
<span class="icon">
<mdi:arrow-up />
</span>
</span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr v-for="container in paginated" :key="container.id">
<td v-if="isVisible('name')">
<router-link :to="{ name: 'container-id', params: { id: container.id } }" :title="container.name">
{{ container.name }}
</router-link>
</td>
<td v-if="isVisible('state')">{{ container.state }}</td>
<td v-if="isVisible('created')">
<distance-time :date="container.created" strict :suffix="false"></distance-time>
</td>
<td v-if="isVisible('cpu')">
<bar-chart :value="container.movingAverage.cpu / 100">
{{ (container.movingAverage.cpu / 100).toLocaleString(undefined, { style: "percent" }) }}
</bar-chart>
</td>
<td v-if="isVisible('mem')">
<bar-chart :value="container.movingAverage.memory / 100">
{{ (container.movingAverage.memory / 100).toLocaleString(undefined, { style: "percent" }) }}
</bar-chart>
</td>
</tr>
</tbody>
</table>
<nav class="pagination is-right" role="navigation" aria-label="pagination" v-if="isPaginated">
<ul class="pagination-list">
<li v-for="i in totalPages">
<a class="pagination-link" :class="{ 'is-current': i === currentPage }" @click.prevent="currentPage = i">{{
i
}}</a>
</li>
</ul>
</nav>
</template>
<script setup lang="ts">
import { Container } from "@/models/Container";
import { toRefs } from "@vueuse/core";
const fields = {
name: {
label: "label.container-name",
sortFunc: (a: Container, b: Container) => a.name.localeCompare(b.name) * direction.value,
mobileVisible: true,
},
state: {
label: "label.status",
sortFunc: (a: Container, b: Container) => a.state.localeCompare(b.state) * direction.value,
mobileVisible: false,
},
created: {
label: "label.created",
sortFunc: (a: Container, b: Container) => (a.created.getTime() - b.created.getTime()) * direction.value,
mobileVisible: true,
},
cpu: {
label: "label.avg-cpu",
sortFunc: (a: Container, b: Container) => (a.movingAverage.cpu - b.movingAverage.cpu) * direction.value,
mobileVisible: false,
},
mem: {
label: "label.avg-mem",
sortFunc: (a: Container, b: Container) => (a.movingAverage.memory - b.movingAverage.memory) * direction.value,
mobileVisible: false,
},
};
const { containers, perPage = 15 } = defineProps<{
containers: Container[];
perPage?: number;
}>();
type keys = keyof typeof fields;
const storage = useStorage<{ column: keys; direction: 1 | -1 }>("DOZZLE_TABLE_CONTAINERS_SORT", {
column: "created",
direction: -1,
});
const { column: sortField, direction } = toRefs(storage);
const sortedContainers = computedWithControl(
() => [containers.length, sortField.value, direction.value],
() => {
return containers.sort((a, b) => {
return fields[sortField.value].sortFunc(a, b);
});
},
);
const totalPages = computed(() => Math.ceil(sortedContainers.value.length / perPage));
const isPaginated = computed(() => totalPages.value > 1);
const currentPage = ref(1);
const paginated = computed(() => {
const start = (currentPage.value - 1) * perPage;
const end = start + perPage;
return sortedContainers.value.slice(start, end);
});
function sort(field: keys) {
if (sortField.value === field) {
direction.value *= -1;
} else {
sortField.value = field;
direction.value = 1;
}
}
function isVisible(field: keys) {
return fields[field].mobileVisible || !isMobile.value;
}
</script>
<style lang="scss" scoped>
.icon {
display: none;
transition: transform 0.2s ease-in-out;
[data-direction="desc"] & {
transform: rotate(180deg);
}
}
.selected-sort {
font-weight: bold;
border-color: var(--primary-color);
.icon {
display: inline-block;
}
}
tbody td {
max-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
+9 -5
View File
@@ -1,11 +1,13 @@
<template>
<div class="dropdown is-hoverable">
<div class="dropdown-trigger">
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu">
<span class="icon">
<mdi-dots-vertical />
</span>
</button>
<slot name="trigger">
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu">
<span class="icon">
<mdi:dots-vertical />
</span>
</button>
</slot>
</div>
<div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-content">
@@ -26,12 +28,14 @@
& > .icon {
height: 100%;
& > svg {
align-self: flex-start;
height: 0.85em;
}
}
}
.is-top {
& .dropdown-menu {
top: 0;
+12 -13
View File
@@ -14,19 +14,17 @@
<div class="media">
<div class="media-left">
<span class="icon is-small" :class="item.state">
<octicon-container-24 />
<octicon:container-24 />
</span>
</div>
<div class="media-content">
{{ item.name }}
</div>
<div class="media-content">{{ item.host }} / {{ item.name }}</div>
<div class="media-right">
<span
class="icon is-small column-icon"
@click.stop.prevent="addColumn(item)"
:title="$t('tooltip.pin-column')"
>
<cil-columns />
<cil:columns />
</span>
</div>
</div>
@@ -43,9 +41,7 @@ const { maxResults: resultLimit = 20 } = defineProps<{
maxResults?: number;
}>();
const emit = defineEmits<{
(e: "close"): void;
}>();
const close = defineEmit();
const query = ref("");
const autocomplete = ref<HTMLElement>();
@@ -54,12 +50,13 @@ const store = useContainerStore();
const { containers } = storeToRefs(store);
const list = computed(() => {
return containers.value.map(({ id, created, name, state }) => {
return containers.value.map(({ id, created, name, state, host }) => {
return {
id,
created,
name,
state,
host,
};
});
});
@@ -91,11 +88,11 @@ watchOnce(autocomplete, () => autocomplete.value?.focus());
function selected({ id }: { id: string }) {
router.push({ name: "container-id", params: { id } });
emit("close");
close();
}
function addColumn(container: Container) {
store.appendActiveContainer(container);
emit("close");
close();
}
</script>
@@ -109,8 +106,8 @@ function addColumn(container: Container) {
.panel {
min-height: 200px;
width: auto;
margin-left: 0.25rem!important;
margin-right: 0.25rem!important;
margin-left: 0.25rem !important;
margin-right: 0.25rem !important;
}
}
@@ -130,9 +127,11 @@ function addColumn(container: Container) {
:deep(a.dropdown-item) {
padding-right: 1em;
.media-right {
visibility: hidden;
}
&:hover .media-right {
visibility: visible;
}
+25
View File
@@ -0,0 +1,25 @@
<template>
<span class="icon-text">
<span class="icon mx-0" v-if="modifiers.includes('shift')">
<carbon:mac-shift />
</span>
<span class="icon mx-0" v-if="modifiers.includes('meta')">
<ph:command v-if="isMac" />
<ph:control-bold v-else />
</span>
<kbd class="is-uppercase is-family-sans-serif ml-1">{{ char }}</kbd>
</span>
</template>
<script lang="ts">
const isMac = /(Mac|iPhone|iPod|iPad)/i.test(navigator.userAgent);
</script>
<script lang="ts" setup>
const { modifiers = ["meta"], char } = defineProps<{ char: string; modifiers?: ("^" | "meta" | "shift")[] }>();
</script>
<style scoped lang="scss">
.icon {
width: unset;
}
</style>
+22 -9
View File
@@ -1,14 +1,25 @@
<template>
<div class="columns is-1 is-variable">
<div class="columns is-1 is-variable is-mobile">
<div class="column is-narrow" v-if="showStd">
<log-std :std="logEntry.std"></log-std>
</div>
<div class="column is-narrow" v-if="showTimestamp">
<log-date :date="logEntry.date"></log-date>
</div>
<div class="column is-narrow is-flex">
<log-level :level="logEntry.level"></log-level>
</div>
<div class="column">
<ul class="fields" :class="{ expanded }" @click="expanded = !expanded">
<li v-for="(value, name) in validValues(logEntry.message)">
<span class="has-text-grey">{{ name }}=</span>
<span class="has-text-weight-bold" v-html="markSearch(value)"></span>
<ul class="fields" :class="{ expanded }" @click="expandToggle()">
<li v-for="(value, name) in validValues">
<span class="has-text-grey">{{ name }}=</span
><span class="has-text-weight-bold" v-if="value === null">&lt;null&gt;</span>
<template v-else-if="Array.isArray(value)">
<span class="has-text-weight-bold" v-html="markSearch(JSON.stringify(value))"> </span>
</template>
<span class="has-text-weight-bold" v-html="markSearch(value)" v-else></span>
</li>
<li class="has-text-grey" v-if="Object.keys(validValues).length === 0">all values are hidden</li>
</ul>
<field-list :fields="logEntry.unfilteredMessage" :expanded="expanded" :visible-keys="visibleKeys"></field-list>
</div>
@@ -24,11 +35,11 @@ const { logEntry } = defineProps<{
visibleKeys: string[][];
}>();
let expanded = $ref(false);
const [expanded, expandToggle] = useToggle();
function validValues(obj: Record<string, any>) {
return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== undefined));
}
const validValues = computed(() => {
return Object.fromEntries(Object.entries(logEntry.message).filter(([_, value]) => value !== undefined));
});
</script>
<style lang="scss" scoped>
@@ -38,6 +49,7 @@ function validValues(obj: Record<string, any>) {
&:hover {
cursor: pointer;
&::after {
content: "expand json";
color: var(--secondary-color);
@@ -55,6 +67,7 @@ function validValues(obj: Record<string, any>) {
li {
display: inline-block;
& + li {
margin-left: 1em;
}
@@ -0,0 +1,27 @@
<template>
<div class="icon is-small health" :health="health" v-if="health" :title="health">
<cil:check-circle v-if="health == 'healthy'" />
<cil:x-circle v-else-if="health == 'unhealthy'" />
<cil:circle v-else />
</div>
</template>
<script lang="ts" setup>
import { ContainerHealth } from "@/types/Container";
defineProps<{
health: ContainerHealth | undefined;
}>();
</script>
<style lang="scss" scoped>
.health {
&[health="unhealthy"] {
color: var(--red-color);
}
&[health="healthy"] {
color: var(--green-color);
}
}
</style>
@@ -0,0 +1,26 @@
<template>
<div>
<span class="has-text-weight-light"> RUNNING </span>
<span class="has-text-weight-semibold">
<distance-time :date="container.created" strict :suffix="false"></distance-time>
</span>
</div>
<div>
<span class="has-text-weight-light"> LOAD </span>
<span class="has-text-weight-semibold"> {{ container.stat.cpu }}% </span>
</div>
<div>
<span class="has-text-weight-light"> MEM </span>
<span class="has-text-weight-semibold"> {{ formatBytes(container.stat.memoryUsage) }} </span>
</div>
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
const { container } = defineProps<{
container: Container;
}>();
</script>
<style lang="scss" scoped></style>
+8 -14
View File
@@ -4,13 +4,13 @@
class="column is-narrow"
:data="memoryData"
label="mem"
:stat-value="formatBytes(container.stat.memoryUsage)"
:stat-value="formatBytes(unref(container.stat).memoryUsage)"
></stat-monitor>
<stat-monitor
class="column is-narrow"
:data="cpuData"
label="load"
:stat-value="container.stat.cpu + '%'"
:stat-value="unref(container.stat).cpu + '%'"
></stat-monitor>
</div>
</template>
@@ -22,39 +22,33 @@ import { type ComputedRef } from "vue";
const container = inject("container") as ComputedRef<Container>;
const cpuData = computedWithControl(
() => container.value.getLastStat(),
() => container.value.stat,
() => {
const history = container.value.getStatHistory();
const history = container.value.statHistory;
const points: Point<unknown>[] = history.map((stat, i) => ({
x: i,
y: stat.snapshot.cpu,
value: stat.snapshot.cpu + "%",
}));
return points;
}
},
);
const memoryData = computedWithControl(
() => container.value.getLastStat(),
() => container.value.stat,
() => {
const history = container.value.getStatHistory();
const history = container.value.statHistory;
const points: Point<string>[] = history.map((stat, i) => ({
x: i,
y: stat.snapshot.memory,
value: formatBytes(stat.snapshot.memoryUsage),
}));
return points;
}
},
);
</script>
<style lang="scss" scoped>
.has-spacer {
&::after {
content: " ";
}
}
.has-border {
border: 1px solid var(--primary-color);
border-radius: 3px;
+36 -5
View File
@@ -1,9 +1,17 @@
<template>
<div class="columns is-marginless has-text-weight-bold is-family-monospace">
<span class="column is-ellipsis">
{{ container.name }}
<span class="tag is-dark">{{ container.image.replace(/@sha.*/, "") }}</span>
</span>
<div class="column is-ellipsis">
<container-health :health="container.health" v-if="container.health"></container-health>
<div class="name">
<span>{{ container.name }}</span
><span v-if="container.isSwarm" class="swarm-id">{{ container.swarmId }}</span>
</div>
<tag class="is-hidden-mobile">{{ container.image.replace(/@sha.*/, "") }}</tag>
<span class="icon is-clickable" @click="togglePinnedContainer(container.storageKey)">
<carbon:star-filled v-if="pinned" />
<carbon:star v-else />
</span>
</div>
</div>
</template>
@@ -12,6 +20,29 @@ import { Container } from "@/models/Container";
import { type ComputedRef } from "vue";
const container = inject("container") as ComputedRef<Container>;
const pinned = computed(() => pinnedContainers.value.has(container.value.storageKey));
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.icon {
vertical-align: middle;
}
.name {
display: inline-flex;
.swarm-id {
max-width: 0;
display: inline-block;
overflow: hidden;
white-space: nowrap;
transition: max-width 0.2s ease-in-out;
will-change: max-width;
}
&:hover {
.swarm-id {
max-width: 400px;
}
}
}
</style>
+2 -3
View File
@@ -14,14 +14,13 @@
<a @click="toggleField(name)"> {{ hasField(name) ? "remove" : "add" }}&nbsp;</a>
<span class="has-text-grey">{{ name }}=</span>[
<span class="has-text-weight-bold" v-for="(item, index) in value">
{{ item }}
<span v-if="index !== value.length - 1">,</span>
<span v-html="JSON.stringify(item)"></span><span v-if="index !== value.length - 1">,</span>
</span>
]
</template>
<template v-else>
<a @click="toggleField(name)"> {{ hasField(name) ? "remove" : "add" }}&nbsp;</a>
<span class="has-text-grey">{{ name }}=</span><span class="has-text-weight-bold">{{ value }}</span>
<span class="has-text-grey">{{ name }}=</span><span class="has-text-weight-bold" v-html="value"></span>
</template>
</li>
</ul>
@@ -1,22 +1,31 @@
<template>
<dropdown-menu class="is-right">
<a class="dropdown-item" @click="onClearClicked">
<template #trigger>
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu">
<span class="icon">
<carbon:circle-solid class="is-red is-small" v-if="streamConfig.stderr" />
<carbon:circle-solid class="is-blue is-small" v-if="streamConfig.stdout" />
</span>
</button>
</template>
<a class="dropdown-item" @click="clear()">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<octicon-trash-24 class="mr-4" />
<octicon:trash-24 />
</div>
</div>
<div class="level-right">
<div class="level-right is-justify-content-space-between is-flex-grow-1">
<div class="level-item">{{ $t("toolbar.clear") }}</div>
<div class="level-item"><key-shortcut char="k" :modifiers="['shift', 'meta']"></key-shortcut></div>
</div>
</div>
</a>
<a class="dropdown-item" :href="`${base}/api/logs/download?id=${container.id}`">
<a class="dropdown-item" :href="`${base}/api/logs/download/${container.host}/${container.id}`">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<octicon-download-24 class="mr-4" />
<octicon:download-24 />
</div>
</div>
<div class="level-right">
@@ -29,11 +38,73 @@
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<mdi-light-magnify class="mr-4" />
<mdi:light-magnify />
</div>
</div>
<div class="level-right is-justify-content-space-between is-flex-grow-1">
<div class="level-item">{{ $t("toolbar.search") }}</div>
<div class="level-item"><key-shortcut char="f"></key-shortcut></div>
</div>
</div>
</a>
<hr class="dropdown-divider" />
<a
class="dropdown-item"
@click="
streamConfig.stdout = true;
streamConfig.stderr = true;
"
>
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<template v-if="streamConfig.stderr && streamConfig.stdout">
<carbon:circle-solid class="is-red is-small" />
<carbon:circle-solid class="is-blue is-small" />
</template>
</div>
</div>
<div class="level-right">
<div class="level-item">{{ $t("toolbar.search") }}</div>
{{ $t("toolbar.show-all") }}
</div>
</div>
</a>
<a
class="dropdown-item"
@click="
streamConfig.stdout = true;
streamConfig.stderr = false;
"
>
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<carbon:circle-solid class="is-blue is-small" v-if="!streamConfig.stderr && streamConfig.stdout" />
</div>
</div>
<div class="level-right">
{{ $t("toolbar.show", { std: "STDOUT" }) }}
</div>
</div>
</a>
<a
class="dropdown-item"
@click="
streamConfig.stdout = false;
streamConfig.stderr = true;
"
>
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<carbon:circle-solid class="is-red is-small" v-if="streamConfig.stderr && !streamConfig.stdout" />
</div>
</div>
<div class="level-right">
<div class="level-item">
{{ $t("toolbar.show", { std: "STDERR" }) }}
</div>
</div>
</div>
</a>
@@ -47,23 +118,20 @@ import { Container } from "@/models/Container";
const { showSearch } = useSearchFilter();
const { base } = config;
const { onClearClicked = (e: Event) => {} } = defineProps<{
onClearClicked: (e: Event) => void;
}>();
const clear = defineEmit();
const container = inject("container") as ComputedRef<Container>;
const streamConfig = inject("stream-config") as { stdout: boolean; stderr: boolean };
</script>
<style lang="scss" scoped>
#download.button,
#clear.button {
.icon {
height: 80%;
}
.level-left .level-item {
width: 2.2em;
align-items: center;
margin-right: 0.5em;
}
&:hover {
color: var(--primary-color);
border-color: var(--primary-color);
}
.is-small {
width: 0.6em;
}
</style>
+15 -7
View File
@@ -1,7 +1,7 @@
<template>
<scrollable-view :scrollable="scrollable" v-if="container">
<template #header v-if="showTitle">
<div class="mr-0 columns is-vcentered is-marginless is-hidden-mobile has-boxshadow">
<div class="mr-0 columns is-mobile is-vcentered is-marginless has-boxshadow">
<div class="column is-clipped is-paddingless">
<container-title @close="$emit('close')" />
</div>
@@ -9,11 +9,11 @@
<container-stat />
</div>
<div class="mr-2 column is-narrow is-paddingless">
<log-actions-toolbar :onClearClicked="onClearClicked" />
<div class="mr-2 column is-narrow is-paddingless is-hidden-mobile">
<log-actions-toolbar @clear="onClearClicked()" />
</div>
<div class="mr-2 column is-narrow is-paddingless" v-if="closable">
<button class="delete is-medium" @click="emit('close')"></button>
<button class="delete is-medium" @click="close()"></button>
</div>
</div>
</template>
@@ -38,26 +38,34 @@ const {
closable?: boolean;
}>();
const emit = defineEmits<{
(event: "close"): void;
}>();
const close = defineEmit();
const store = useContainerStore();
const container = store.currentContainer($$(id));
const config = reactive({ stdout: true, stderr: true });
provide("container", container);
provide("stream-config", config);
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>
<style lang="scss" scoped>
button.delete {
background-color: var(--scheme-main-ter);
opacity: 0.6;
&:after,
&:before {
background-color: var(--text-color);
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<relative-time :date="date" class="date"></relative-time>
<date-time :date="date" class="date"></date-time>
</template>
<script lang="ts" setup>
defineProps<{
@@ -12,7 +12,7 @@ import { createRouter, createWebHistory } from "vue-router";
vi.mock("@/stores/config", () => ({
__esModule: true,
default: { base: "" },
default: { base: "", hosts: ["localhost"] },
}));
/**
@@ -44,11 +44,11 @@ describe("<LogEventSource />", () => {
hourStyle = "auto",
}: { searchFilter?: string | undefined; hourStyle?: "auto" | "24" | "12" } = {
hourStyle: "auto",
}
},
) {
settings.value.hourStyle = hourStyle;
search.searchFilter.value = searchFilter;
if(searchFilter){
if (searchFilter) {
search.showSearch.value = true;
}
@@ -71,7 +71,8 @@ describe("<LogEventSource />", () => {
LogViewer,
},
provide: {
container: computed(() => ({ id: "abc", image: "test:v123" })),
container: computed(() => ({ id: "abc", image: "test:v123", host: "localhost" })),
"stream-config": reactive({ stdout: true, stderr: true }),
scrollingPaused: computed(() => false),
},
},
@@ -84,6 +85,8 @@ describe("<LogEventSource />", () => {
});
}
const sourceUrl = "/api/logs/stream/localhost/abc?lastEventId=&stdout=1&stderr=1";
test("renders correctly", async () => {
const wrapper = createLogEventSource();
expect(wrapper.html()).toMatchSnapshot();
@@ -91,22 +94,22 @@ describe("<LogEventSource />", () => {
test("should connect to EventSource", async () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
expect(sources["/api/logs/stream?id=abc&lastEventId="].readyState).toBe(1);
sources[sourceUrl].emitOpen();
expect(sources[sourceUrl].readyState).toBe(1);
wrapper.unmount();
});
test("should close EventSource", async () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources[sourceUrl].emitOpen();
wrapper.unmount();
expect(sources["/api/logs/stream?id=abc&lastEventId="].readyState).toBe(2);
expect(sources[sourceUrl].readyState).toBe(2);
});
test("should parse messages", async () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"This is a message.", "id":1}`,
});
@@ -121,8 +124,8 @@ describe("<LogEventSource />", () => {
describe("render html correctly", () => {
test("should render messages", async () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"This is a message.", "id":1}`,
});
@@ -134,8 +137,8 @@ describe("<LogEventSource />", () => {
test("should render messages with color", async () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({
data: '{"ts":1560336942459,"m":"\\u001b[30mblack\\u001b[37mwhite", "id":1}',
});
@@ -147,8 +150,8 @@ describe("<LogEventSource />", () => {
test("should render messages with html entities", async () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
});
@@ -160,8 +163,8 @@ describe("<LogEventSource />", () => {
test("should render dates with 12 hour style", async () => {
const wrapper = createLogEventSource({ hourStyle: "12" });
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
});
@@ -173,8 +176,8 @@ describe("<LogEventSource />", () => {
test("should render dates with 24 hour style", async () => {
const wrapper = createLogEventSource({ hourStyle: "24" });
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
});
@@ -186,11 +189,11 @@ describe("<LogEventSource />", () => {
test("should render messages with filter", async () => {
const wrapper = createLogEventSource({ searchFilter: "test" });
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"foo bar", "id":1}`,
});
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"test bar", "id":2}`,
});
@@ -7,15 +7,14 @@
import { Container } from "@/models/Container";
import { type ComputedRef } from "vue";
const emit = defineEmits<{
(e: "loading-more", value: boolean): void;
}>();
const loadingMore = defineEmit<[value: boolean]>();
const container = inject("container") as ComputedRef<Container>;
const { messages, loadOlderLogs } = useLogStream(container);
const config = inject("stream-config") as { stdout: boolean; stderr: boolean };
const { messages, loadOlderLogs } = useLogStream(container, config);
const beforeLoading = () => emit("loading-more", true);
const afterLoading = () => emit("loading-more", false);
const beforeLoading = () => loadingMore(true);
const afterLoading = () => loadingMore(false);
defineExpose({
clear: () => (messages.value = []),
+63
View File
@@ -0,0 +1,63 @@
<template>
<div :class="level" :data-position="position"></div>
</template>
<script lang="ts" setup>
import { Position } from "@/models/LogEntry";
defineProps<{
level?: string;
position?: Position;
}>();
</script>
<style lang="scss" scoped>
div {
display: inline-block;
width: 0.7em;
height: 0.7em;
border-radius: 0.5em;
align-self: auto;
margin-top: 0.4em;
&[data-position="start"] {
border-radius: 0.5em 0.5em 0 0;
height: 70%;
margin-bottom: -0.2em;
margin-top: auto;
align-self: flex-end;
}
&[data-position="middle"] {
border-radius: 0;
height: auto;
margin: -0.2em 0;
align-self: auto;
}
&[data-position="end"] {
border-radius: 0 0 0.5em 0.5em;
height: 70%;
margin-top: -0.2em;
align-self: flex-start;
}
&.debug,
&.trace {
background-color: var(--purple-color);
}
&.info {
background-color: var(--green-color);
}
&.error,
&.fatal {
background-color: var(--red-color);
}
&.warn,
&.warning {
background-color: var(--orange-color);
}
}
</style>
+25
View File
@@ -0,0 +1,25 @@
<template>
<tag size="small" :std="std">
{{ std }}
</tag>
</template>
<script lang="ts" setup>
import { Std } from "@/models/LogEntry";
defineProps<{
std: Std;
}>();
</script>
<style lang="scss" scoped>
.tag {
&[std="stdout"] {
color: var(--blue-color);
}
&[std="stderr"] {
color: var(--red-color);
}
}
</style>
+16 -4
View File
@@ -12,7 +12,7 @@
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<cil-find-in-page class="mr-4" />
<cil:find-in-page class="mr-4" />
</div>
</div>
<div class="level-right">
@@ -58,24 +58,36 @@ const routeHash = useRouteHash();
watch(
routeHash,
(hash) => {
document.querySelector(`[data-key="${hash.substring(1)}"]`)?.scrollIntoView({ block: "center" });
if (hash) {
document.querySelector(`[data-key="${hash.substring(1)}"]`)?.scrollIntoView({ block: "center" });
}
},
{ immediate: true, flush: "post" }
{ immediate: true, flush: "post" },
);
</script>
<style scoped lang="scss">
.events {
padding: 1em 0;
font-family: SFMono-Regular, Consolas, Liberation Mono, monaco, Menlo, monospace;
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Consolas,
Liberation Mono,
monaco,
Menlo,
monospace;
& > li {
display: flex;
word-wrap: break-word;
padding: 0.2em 1em;
&:last-child {
scroll-snap-align: end;
scroll-margin-block-end: 5rem;
}
&:nth-child(odd) {
background-color: rgba(125, 125, 125, 0.08);
}
@@ -1,5 +1,5 @@
<template>
<log-event-source ref="source" #default="{ messages }" @loading-more="emit('loading-more', $event)">
<log-event-source ref="source" #default="{ messages }" @loading-more="loadingMore($event)">
<log-viewer :messages="messages"></log-viewer>
</log-event-source>
</template>
@@ -7,10 +7,7 @@
<script lang="ts" setup>
import LogEventSource from "./LogEventSource.vue";
const emit = defineEmits<{
(e: "loading-more", value: boolean): void;
}>();
const loadingMore = defineEmit<[value: boolean]>();
const source = $ref<InstanceType<typeof LogEventSource>>();
function clear() {
source?.clear();
@@ -1,8 +1,14 @@
<template>
<div class="columns is-1 is-variable">
<div class="columns is-1 is-variable is-mobile">
<div class="column is-narrow" v-if="showStd">
<log-std :std="logEntry.std"></log-std>
</div>
<div class="column is-narrow" v-if="showTimestamp">
<log-date :date="logEntry.date"></log-date>
</div>
<div class="column is-narrow is-flex">
<log-level :level="logEntry.level" :position="logEntry.position"></log-level>
</div>
<div class="text column" v-html="colorize(logEntry.message)"></div>
</div>
</template>
@@ -10,7 +16,7 @@
import { SimpleLogEntry } from "@/models/LogEntry";
import AnsiConvertor from "ansi-to-html";
const ansiConvertor = new AnsiConvertor({ escapeXML: true });
const ansiConvertor = new AnsiConvertor({ escapeXML: false, fg: "var(--text-color)" });
defineProps<{
logEntry: SimpleLogEntry;
}>();
+9 -13
View File
@@ -1,10 +1,10 @@
<template>
<div class="has-text-centered is-relative host" @mouseenter="mouseOver = true" @mouseleave="mouseOver = false">
<div class="has-border has-boxshadow">
<div class="has-border has-boxshadow is-hidden-mobile">
<stat-sparkline :data="data" @selected-point="onSelectedPoint"></stat-sparkline>
</div>
<div class="has-background-body-color is-top-left">
<span class="has-text-weight-light has-spacer">{{ label }}</span>
<span class="has-text-weight-light">{{ label }}</span>
<span class="has-text-weight-bold">
{{ mouseOver ? selectedPoint?.value ?? selectedPoint?.y ?? statValue : statValue }}
</span>
@@ -15,7 +15,7 @@
<script lang="ts" setup>
const { data, label, statValue } = defineProps<{ data: Point<unknown>[]; label: string; statValue: string | number }>();
let selectedPoint: Point<unknown> = $ref();
let selectedPoint: Point<unknown> | undefined = $ref();
function onSelectedPoint(point: Point<unknown>) {
selectedPoint = point;
@@ -25,12 +25,6 @@ let mouseOver = $ref(false);
</script>
<style lang="scss" scoped>
.has-spacer {
&::after {
content: " ";
}
}
.has-border {
border: 1px solid var(--primary-color);
border-radius: 3px;
@@ -48,9 +42,11 @@ let mouseOver = $ref(false);
color: var(--secondary-color);
}
.is-top-left {
position: absolute;
top: 0;
left: 0.75em;
@media screen and (min-width: 768px) {
.is-top-left {
position: absolute;
top: 0;
left: 0.75em;
}
}
</style>
@@ -15,9 +15,7 @@ const { data, width = 150, height = 30 } = defineProps<{ data: Point<unknown>[];
const x = d3.scaleLinear().range([width, 0]);
const y = d3.scaleLinear().range([height, 0]);
const emit = defineEmits<{
(event: "selected-point", value: Point<unknown>): void;
}>();
const selectedPoint = defineEmit<[value: Point<unknown>]>();
const shape = d3
.area<Point<unknown>>()
@@ -28,7 +26,7 @@ const shape = d3
const path = computed(() => {
x.domain(d3.extent(data, (d) => d.x) as [number, number]);
y.domain(d3.extent(data, (d) => d.y) as [number, number]);
y.domain(d3.extent([...data, { y: 1 }], (d) => d.y) as [number, number]);
return shape(data) ?? "";
});
@@ -41,7 +39,7 @@ function onMove(e: MouseEvent) {
const index = Math.round(xValue);
lineX = x(index);
const point = data[index];
emit("selected-point", point);
selectedPoint(point);
}
</script>
@@ -1,200 +1,237 @@
// Vitest Snapshot v1
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<LogEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
<div class=\\"text column\\" data-v-a49e52d4=\\"\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render dates with 24 hour style 1`] = `
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42</time></div>
<div class=\\"text column\\" data-v-a49e52d4=\\"\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages 1`] = `
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
<div class=\\"text column\\" data-v-a49e52d4=\\"\\">This is a message.</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\">This is a message.</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with color 1`] = `
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
<div class=\\"text column\\" data-v-a49e52d4=\\"\\"><span style=\\"color:#000\\">black<span style=\\"color:#AAA\\">white</span></span></div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\"><span style=\\"color:#000\\">black<span style=\\"color:#AAA\\">white</span></span></div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with filter 1`] = `
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
<li data-key=\\"2\\" class=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"line-options\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2\\" data-v-2e92daca=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"2\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#2\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
<div class=\\"text column\\" data-v-a49e52d4=\\"\\"><mark>test</mark> bar</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\"><mark>test</mark> bar</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with html entities 1`] = `
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
<div class=\\"text column\\" data-v-a49e52d4=\\"\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > renders correctly 1`] = `
"<div class=\\"infinte-loader\\" data-v-1cd63c6e=\\"\\">
<div class=\\"spinner\\" data-v-1cd63c6e=\\"\\" style=\\"display: none;\\">
<div class=\\"bounce1\\" data-v-1cd63c6e=\\"\\"></div>
<div class=\\"bounce2\\" data-v-1cd63c6e=\\"\\"></div>
<div class=\\"bounce3\\" data-v-1cd63c6e=\\"\\"></div>
"<div data-v-1cd63c6e=\\"\\" class=\\"infinte-loader\\">
<div data-v-1cd63c6e=\\"\\" class=\\"spinner\\" style=\\"display: none;\\">
<div data-v-1cd63c6e=\\"\\" class=\\"bounce1\\"></div>
<div data-v-1cd63c6e=\\"\\" class=\\"bounce2\\"></div>
<div data-v-1cd63c6e=\\"\\" class=\\"bounce3\\"></div>
</div>
</div>
<ul class=\\"events medium\\" data-v-2e92daca=\\"\\"></ul>"
<ul data-v-2e92daca=\\"\\" class=\\"events medium\\"></ul>"
`;
exports[`<LogEventSource /> > should parse messages 1`] = `
@@ -202,5 +239,8 @@ SimpleLogEntry {
"_message": "This is a message.",
"date": 2019-06-12T10:55:42.459Z,
"id": 1,
"level": undefined,
"position": undefined,
"std": "stderr",
}
`;
-19
View File
@@ -1,19 +0,0 @@
<template>
<time :datetime="date.toISOString()">{{ text }}</time>
</template>
<script lang="ts" setup>
import formatDistance from "date-fns/formatDistance";
const { date } = defineProps<{
date: Date;
}>();
const text = ref<string>();
function updateFromNow() {
text.value = formatDistance(date, new Date(), {
addSuffix: true,
});
}
useIntervalFn(updateFromNow, 30_000, { immediateCallback: true });
</script>
+51
View File
@@ -0,0 +1,51 @@
<template>
<div @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" ref="trigger"><slot></slot></div>
<Teleport to="body">
<Transition name="fade">
<div v-show="show && (delayedShow || glopbalShow)" class="content" ref="content">
<slot name="content"></slot>
</div>
</Transition>
</Teleport>
</template>
<script lang="ts" setup>
import { globalShowPopup } from "@/composables/popup";
let glopbalShow = globalShowPopup();
let show = ref(glopbalShow.value);
let delayedShow = refDebounced(show, 1000);
let content: HTMLElement | null = $ref(null);
let trigger: HTMLElement | null = $ref(null);
function onMouseEnter(e: MouseEvent) {
show.value = true;
glopbalShow.value = true;
if (e.target && content && e.target instanceof Element) {
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`;
}
}
function onMouseLeave() {
show.value = false;
glopbalShow.value = false;
}
</script>
<style scoped>
.content {
position: fixed;
z-index: 9999;
background: var(--scheme-main-ter);
border-radius: 0.5em;
padding: 1em;
box-shadow: 0 0 0.5em rgba(0, 0, 0, 0.5);
border: 1px solid var(--border-color);
}
</style>
-21
View File
@@ -1,21 +0,0 @@
<template>
<time :datetime="date.toISOString()">{{ format(date) }}</time>
</template>
<script lang="ts" setup>
defineProps<{
date: Date;
}>();
const dateFormatter = new Intl.DateTimeFormat(undefined, { day: "2-digit", month: "2-digit", year: "numeric" });
const use12Hour = $computed(() => ({ auto: undefined, "12": true, "24": false }[hourStyle.value]));
const timeFormatter = $computed(
() => new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit", second: "2-digit", hour12: use12Hour })
);
function format(date: Date) {
const dateStr = dateFormatter.format(date);
const timeStr = timeFormatter.format(date);
return `${dateStr} ${timeStr}`;
}
</script>
+31 -30
View File
@@ -1,20 +1,22 @@
<template>
<div class="scroll-progress" ref="root">
<svg width="100" height="100" viewBox="0 0 100 100" :class="{ indeterminate }">
<circle r="44" cx="50" cy="50" />
</svg>
<div class="is-overlay columns is-vcentered is-centered has-text-weight-light">
<template v-if="indeterminate">
<div class="column is-narrow is-paddingless is-size-2">&#8734;</div>
</template>
<template v-else>
<span class="column is-narrow is-paddingless is-size-2">
{{ Math.ceil(scrollProgress * 100) }}
</span>
<span class="column is-narrow is-paddingless"> % </span>
</template>
<transition name="fade">
<div class="scroll-progress" 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" />
</svg>
<div class="is-overlay columns is-vcentered is-centered has-text-weight-light">
<template v-if="indeterminate">
<div class="column is-narrow is-paddingless is-size-2">&#8734;</div>
</template>
<template v-else-if="!isNaN(scrollProgress)">
<span class="column is-narrow is-paddingless is-size-2">
{{ Math.ceil(scrollProgress * 100) }}
</span>
<span class="column is-narrow is-paddingless"> % </span>
</template>
</div>
</div>
</div>
</transition>
</template>
<script lang="ts" setup>
@@ -24,12 +26,12 @@ const { indeterminate = false, autoHide = false } = defineProps<{
}>();
const scrollProgress = ref(0);
const animation = ref({ cancel: () => {} });
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, { throttle: 100 });
const { y: scrollY } = useScroll(scrollElement as Ref<HTMLElement | Document>, { throttle: 100 });
const show = autoResetRef(false, 2000);
onMounted(() => {
watch(
@@ -37,7 +39,7 @@ onMounted(() => {
() => {
scrollElement.value = (root.value?.closest("[data-scrolling]") as HTMLElement) ?? document;
},
{ immediate: true, flush: "post" }
{ immediate: true, flush: "post" },
);
});
@@ -46,18 +48,9 @@ watchPostEffect(() => {
scrollElement.value === document
? (scrollElement.value as Document).documentElement
: (scrollElement.value as HTMLElement);
scrollProgress.value = scrollY.value / (parent.scrollHeight - parent.clientHeight);
animation.value.cancel();
if (autoHide && root.value) {
animation.value = root.value.animate(
{ opacity: [1, 0] },
{
duration: 500,
delay: 2000,
fill: "both",
easing: "ease-out",
}
);
scrollProgress.value = Math.max(0, Math.min(1, scrollY.value / (parent.scrollHeight - parent.clientHeight)));
if (autoHide) {
show.value = true;
}
});
</script>
@@ -119,4 +112,12 @@ watchPostEffect(() => {
transform: rotate(360deg);
}
}
.fade-leave-active {
transition: opacity 0.2s ease-in-out;
}
.fade-leave-to {
opacity: 0;
}
</style>
+22 -4
View File
@@ -5,7 +5,7 @@
</header>
<main :data-scrolling="scrollable ? true : undefined">
<div class="is-scrollbar-progress is-hidden-mobile">
<scroll-progress v-show="paused" :indeterminate="loading" :auto-hide="!loading"></scroll-progress>
<scroll-progress v-if="paused" :indeterminate="loading" :auto-hide="!loading"></scroll-progress>
</div>
<div ref="scrollableContent">
<slot :setLoading="setLoading"></slot>
@@ -22,7 +22,7 @@
@click="scrollToBottom()"
v-show="paused"
>
<mdi-light-chevron-double-down />
<mdi:light-chevron-double-down />
</button>
</transition>
</div>
@@ -45,7 +45,8 @@ const mutationObserver = new MutationObserver((e) => {
scrollToBottom();
} else {
const record = e[e.length - 1];
if (record.target.children[record.target.children.length - 1] == record.addedNodes[record.addedNodes.length - 1]) {
const children = (record.target as HTMLElement).children;
if (children[children.length - 1] == record.addedNodes[record.addedNodes.length - 1]) {
hasMore = true;
}
}
@@ -81,6 +82,10 @@ section {
background: var(--body-background-color);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
z-index: 1;
@media screen and (max-width: 768px) {
top: 70px;
}
}
&.is-full-height-scrollable {
@@ -97,6 +102,7 @@ section {
.is-scrollbar-progress {
text-align: right;
margin-right: 110px;
.scroll-progress {
position: fixed;
top: 60px;
@@ -111,6 +117,7 @@ section {
.is-scrollbar-notification {
text-align: right;
margin-right: 65px;
button {
position: fixed;
bottom: 30px;
@@ -138,9 +145,11 @@ section {
100% {
transform: translateY(0);
}
40% {
transform: translateY(-30px);
}
60% {
transform: translateY(-15px);
}
@@ -148,11 +157,20 @@ section {
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.15s ease-in;
transition: opacity 0.2s ease-out !important;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
}
</style>
<style>
@media screen and (max-width: 768px) {
.splitpanes__pane {
overflow: unset !important;
}
}
</style>
+6 -2
View File
@@ -11,7 +11,7 @@
@keyup.esc="resetSearch()"
/>
<span class="icon is-left">
<mdi-light-magnify />
<mdi:light-magnify />
</span>
</p>
</div>
@@ -46,12 +46,15 @@ onUnmounted(() => resetSearch());
right: 0;
border-radius: 0 0 0 5px;
z-index: 10;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.12),
0 1px 2px rgba(0, 0, 0, 0.24);
button.delete {
margin-left: 1em;
background-color: var(--scheme-main-ter);
opacity: 0.6;
&:after,
&:before {
background-color: var(--text-color);
@@ -68,6 +71,7 @@ onUnmounted(() => resetSearch());
.input {
color: var(--body-color);
&::placeholder {
color: var(--border-color);
}
+184 -101
View File
@@ -1,130 +1,213 @@
<template>
<aside>
<div class="columns is-marginless">
<div class="column is-paddingless">
<h1>
<router-link :to="{ name: 'index' }">
<svg class="logo">
<use href="#logo"></use>
</svg>
</router-link>
<small class="subtitle is-6 is-block mb-4" v-if="hostname">
{{ hostname }}
</small>
</h1>
</div>
</div>
<div class="columns is-marginless">
<div class="column is-narrow py-0 pl-0 pr-1">
<button class="button is-rounded is-small" @click="$emit('search')" :title="$t('tooltip.search')">
<span class="icon">
<mdi-light-magnify />
</span>
</button>
</div>
<div class="column is-narrow py-0" :class="secured ? 'pl-0 pr-1' : 'px-0'">
<router-link :to="{ name: 'settings' }" active-class="is-active" class="button is-rounded is-small">
<span class="icon">
<mdi-light-cog />
</span>
</router-link>
</div>
<div class="column is-narrow py-0 px-0" v-if="secured">
<a class="button is-rounded is-small" :href="`${base}/logout`" :title="$t('button.logout')">
<span class="icon">
<mdi-light-logout />
</span>
</a>
</div>
</div>
<p class="menu-label is-hidden-mobile">{{ $t("label.containers") }}</p>
<ul class="menu-list is-hidden-mobile" v-if="ready">
<li v-for="item in visibleContainers" :key="item.id" :class="item.state">
<router-link
:to="{ name: 'container-id', params: { id: item.id } }"
active-class="is-active"
:title="item.name"
>
<div class="container is-flex is-align-items-center">
<div class="is-flex-grow-1 is-ellipsis">
{{ item.name }}
</div>
<div class="is-flex-shrink-1 column-icon">
<span
class="icon is-small"
@click.stop.prevent="store.appendActiveContainer(item)"
v-show="!activeContainersById[item.id]"
:title="$t('tooltip.pin-column')"
>
<cil-columns />
</span>
</div>
<div v-if="ready">
<nav class="breadcrumb menu-label" aria-label="breadcrumbs">
<ul v-if="sessionHost">
<li>
<a href="#" @click.prevent="setHost(null)">{{ hosts[sessionHost].name }}</a>
</li>
</ul>
<ul v-else>
<li>Hosts</li>
</ul>
</nav>
<transition :name="sessionHost ? 'slide-left' : 'slide-right'" mode="out-in">
<ul class="menu-list" v-if="!sessionHost">
<li v-for="host in config.hosts">
<a @click.prevent="setHost(host.id)">{{ host.name }}</a>
</li>
</ul>
<transition-group tag="ul" name="list" class="menu-list" v-else>
<li v-for="item in menuItems" :key="item.id" :class="item.state" :data-label="item.id">
<div class="menu-label mt-4 mb-3" v-if="isLabel(item)">
{{ item.label }}
</div>
</router-link>
</li>
</ul>
<ul class="menu-list is-hidden-mobile loading" v-else>
<li v-for="index in 7" class="my-4"><o-skeleton animated size="large" :key="index"></o-skeleton></li>
</ul>
</aside>
<popup v-else>
<router-link
:to="{ name: 'container-id', params: { id: item.id } }"
active-class="is-active"
:title="item.name"
>
<div class="container is-flex is-align-items-center">
<div class="is-flex-grow-1 is-ellipsis">
<span>{{ item.name }}</span
><span class="has-text-weight-light has-light-opacity" v-if="item.isSwarm">{{ item.swarmId }}</span>
</div>
<div class="is-flex-shrink-1 is-flex icons">
<div
class="icon is-small pin"
@click.stop.prevent="store.appendActiveContainer(item)"
v-show="!activeContainersById[item.id]"
:title="$t('tooltip.pin-column')"
>
<cil:columns />
</div>
<container-health :health="item.health"></container-health>
</div>
</div>
</router-link>
<template #content>
<container-popup :container="item"></container-popup>
</template>
</popup>
</li>
</transition-group>
</transition>
</div>
<ul class="menu-list is-hidden-mobile has-light-opacity" v-else>
<li v-for="index in 7" class="my-4"><o-skeleton animated size="large" :key="index"></o-skeleton></li>
</ul>
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
import { sessionHost } from "@/composables/storage";
const { t } = useI18n();
const { base, secured, hostname } = config;
const store = useContainerStore();
const { activeContainers, visibleContainers, ready } = storeToRefs(store);
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.storageKey)) {
acc.pinned.push(item);
} else {
acc.unpinned.push(item);
}
return acc;
},
{ pinned: [] as Container[], unpinned: [] as Container[] },
),
);
type MenuLabel = { label: string; id: string; state: string };
const pinnedLabel = { label: t("label.pinned"), id: "pinned", state: "label" } as MenuLabel;
const allLabel = { label: t("label.containers"), id: "all", state: "label" } as MenuLabel;
function isLabel(item: Container | MenuLabel): item is MenuLabel {
return (item as MenuLabel).label !== undefined;
}
const menuItems = computed(() => {
if (groupedContainers.value.pinned.length > 0) {
return [pinnedLabel, ...groupedContainers.value.pinned, allLabel, ...groupedContainers.value.unpinned];
} else {
return [allLabel, ...groupedContainers.value.unpinned];
}
});
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>)
activeContainers.value.reduce(
(acc, item) => {
acc[item.id] = item;
return acc;
},
{} as Record<string, Container>,
),
);
</script>
<style scoped lang="scss">
aside {
padding: 1em;
height: 100vh;
overflow: auto;
position: fixed;
width: inherit;
.is-hidden-mobile.is-active {
display: block !important;
}
}
.loading {
.has-light-opacity {
opacity: 0.5;
}
li.exited a {
li.exited a,
li.dead a {
color: #777;
}
.logo {
width: 122px;
height: 54px;
fill: var(--logo-color);
.icons {
column-gap: 0.35em;
align-items: baseline;
}
a {
.pin {
display: none;
.menu-list li {
.column-icon {
visibility: hidden;
& > span {
vertical-align: middle;
}
}
&:hover .column-icon {
visibility: visible;
&:hover {
color: var(--secondary-color);
}
}
&:hover {
.pin {
display: block;
}
}
}
.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(100%);
}
.slide-right-enter-from {
opacity: 0;
transform: translateX(-100%);
}
.slide-left-leave-to {
opacity: 0;
transform: translateX(-100%);
}
.slide-right-leave-to {
opacity: 0;
transform: translateX(100%);
}
.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>
+71
View File
@@ -0,0 +1,71 @@
<template>
<aside>
<div class="columns is-marginless">
<div class="column is-paddingless">
<h1>
<router-link :to="{ name: 'index' }">
<svg class="logo">
<use href="#logo"></use>
</svg>
</router-link>
<small class="subtitle is-6 is-block mb-4" v-if="hostname">
{{ hostname }}
</small>
</h1>
</div>
</div>
<div class="columns is-marginless">
<div class="column is-narrow py-0 pl-0 pr-1">
<button class="button is-rounded is-small" @click="$emit('search')" :title="$t('tooltip.search')">
<span class="icon">
<mdi:light-magnify />
</span>
</button>
</div>
<div class="column is-narrow py-0" :class="secured ? 'pl-0 pr-1' : 'px-0'">
<router-link
:to="{ name: 'settings' }"
active-class="is-active"
class="button is-rounded is-small"
:aria-label="$t('title.settings')"
>
<span class="icon">
<mdi:light-cog />
</span>
</router-link>
</div>
<div class="column is-narrow py-0 px-0" v-if="secured">
<a class="button is-rounded is-small" :href="`${base}/logout`" :title="$t('button.logout')">
<span class="icon">
<mdi:light-logout />
</span>
</a>
</div>
</div>
<side-menu class="mt-4"></side-menu>
</aside>
</template>
<script lang="ts" setup>
const { base, secured, hostname } = config;
</script>
<style scoped lang="scss">
aside {
padding: 1em;
height: 100vh;
overflow: auto;
position: fixed;
width: inherit;
.is-hidden-mobile.is-active {
display: block !important;
}
}
.logo {
width: 122px;
height: 54px;
fill: var(--logo-color);
}
</style>
+22
View File
@@ -0,0 +1,22 @@
<template>
<div>
<time :datetime="date.toISOString()" class="is-hidden-mobile">{{ dateStr }}</time>
<time :datetime="date.toISOString()">{{ timeStr }}</time>
</div>
</template>
<script lang="ts" setup>
const props = defineProps<{
date: Date;
}>();
const dateFormatter = new Intl.DateTimeFormat(undefined, { day: "2-digit", month: "2-digit", year: "numeric" });
const use12Hour = $computed(() => ({ auto: undefined, "12": true, "24": false })[hourStyle.value]);
const timeFormatter = $computed(
() =>
new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit", second: "2-digit", hour12: use12Hour }),
);
const dateStr = $computed(() => dateFormatter.format(props.date));
const timeStr = $computed(() => timeFormatter.format(props.date));
</script>
+30
View File
@@ -0,0 +1,30 @@
<template>
<time :datetime="date.toISOString()" data-ci-skip>{{ text }}</time>
</template>
<script lang="ts" setup>
import formatDistanceToNow from "date-fns/formatDistanceToNow";
import formatDistanceToNowStrict from "date-fns/formatDistanceToNowStrict";
const {
date,
strict = false,
suffix = true,
} = defineProps<{
date: Date;
strict?: boolean;
suffix?: boolean;
}>();
const text = ref<string>();
watch($$(date), updateFromNow, { immediate: true });
function updateFromNow() {
const fn = strict ? formatDistanceToNowStrict : formatDistanceToNow;
text.value = fn(date, {
addSuffix: suffix,
});
}
useIntervalFn(updateFromNow, 30_000, { immediateCallback: true });
</script>
@@ -8,9 +8,6 @@
</svg>
</router-link>
</div>
<div class="column ml-4 is-family-monospace is-ellipsis" v-if="$route.name == 'container'">
{{ allContainersById[route.params.id].name }}
</div>
<div class="column is-narrow push-right">
<a
@@ -25,11 +22,28 @@
</div>
<div class="menu-label level is-mobile is-hidden-mobile" :class="{ 'is-active': showNav }">
<div v-if="config.hosts.length > 1">
<o-dropdown v-model="sessionHost" aria-role="list">
<template #trigger>
<o-button variant="primary" type="button" size="small">
<span>{{ sessionHost ? hosts[sessionHost].name : "" }}</span>
<span class="icon">
<carbon:caret-down />
</span>
</o-button>
</template>
<o-dropdown-item :value="value.id" aria-role="listitem" v-for="value in config.hosts" :key="value">
<span>{{ value.name }}</span>
</o-dropdown-item>
</o-dropdown>
</div>
<div class="level-item has-text-centered">
<div>
<button class="button is-small is-rounded" @click="$emit('search')" :title="$t('tooltip.search')">
<span class="icon">
<mdi-light-magnify />
<mdi:light-magnify />
</span>
</button>
</div>
@@ -38,7 +52,7 @@
<div>
<router-link :to="{ name: 'settings' }" active-class="is-active" class="button is-small is-rounded">
<span class="icon">
<mdi-light-cog />
<mdi:light-cog />
</span>
</router-link>
</div>
@@ -47,7 +61,7 @@
<div>
<a class="button is-small is-rounded" :href="`${base}/logout`" :title="$t('button.logout')">
<span class="icon">
<mdi-light-logout />
<mdi:light-logout />
</span>
</a>
</div>
@@ -56,7 +70,7 @@
<p class="menu-label is-hidden-mobile" :class="{ 'is-active': showNav }">{{ $t("label.containers") }}</p>
<ul class="menu-list is-hidden-mobile" :class="{ 'is-active': showNav }">
<li v-for="item in visibleContainers" :key="item.id">
<li v-for="item in sortedContainers" :key="item.id">
<router-link
:to="{ name: 'container-id', params: { id: item.id } }"
active-class="is-active"
@@ -73,15 +87,40 @@
<script lang="ts" setup>
const { base, secured } = config;
import { sessionHost } from "@/composables/storage";
const store = useContainerStore();
const route = useRoute();
const { visibleContainers, allContainersById } = storeToRefs(store);
const { visibleContainers } = storeToRefs(store);
let showNav = $ref(false);
watch(route, () => {
showNav = 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.reduce(
(acc, item) => {
acc[item.id] = item;
return acc;
},
{} as Record<string, { name: string; id: string }>,
),
);
</script>
<style scoped lang="scss">
aside {
+19
View File
@@ -0,0 +1,19 @@
<template>
<div class="tag" :size="size">
<slot></slot>
</div>
</template>
<script lang="ts" setup>
const { size = undefined } = defineProps<{ size?: "small" | undefined }>();
</script>
<style scoped lang="scss">
.tag {
background-color: var(--scheme-main-ter);
border: 1px solid var(--border-color);
&[size="small"] {
font-size: 0.61rem;
}
}
</style>
+51 -6
View File
@@ -1,4 +1,5 @@
import { type ComputedRef, type Ref } from "vue";
import { encodeXML } from "entities";
import debounce from "lodash.debounce";
import {
type LogEvent,
@@ -11,11 +12,21 @@ import {
import { Container } from "@/models/Container";
function parseMessage(data: string): LogEntry<string | JSONObject> {
const e = JSON.parse(data) as LogEvent;
const e = JSON.parse(data, (key, value) => {
if (typeof value === "string") {
return encodeXML(value);
}
return value;
}) as LogEvent;
return asLogEntry(e);
}
export function useLogStream(container: ComputedRef<Container>) {
type LogStreamConfig = {
stdout: boolean;
stderr: boolean;
};
export function useLogStream(container: ComputedRef<Container>, streamConfig: LogStreamConfig) {
let messages: LogEntry<string | JSONObject>[] = $ref([]);
let buffer: LogEntry<string | JSONObject>[] = $ref([]);
const scrollingPaused = $ref(inject("scrollingPaused") as Ref<boolean>);
@@ -58,7 +69,22 @@ export function useLogStream(container: ComputedRef<Container>) {
lastEventId = "";
}
es = new EventSource(`${config.base}/api/logs/stream?id=${container.value.id}&lastEventId=${lastEventId}`);
const params = {
lastEventId,
} as { lastEventId: string; stdout?: string; stderr?: string };
if (streamConfig.stdout) {
params.stdout = "1";
}
if (streamConfig.stderr) {
params.stderr = "1";
}
es = new EventSource(
`${config.base}/api/logs/stream/${container.value.host}/${container.value.id}?${new URLSearchParams(
params,
).toString()}`,
);
es.addEventListener("container-stopped", () => {
es?.close();
es = null;
@@ -85,8 +111,25 @@ export function useLogStream(container: ComputedRef<Container>) {
const last = messages[299].date;
const delta = to.getTime() - last.getTime();
const from = new Date(to.getTime() + delta);
const params = {
from: from.toISOString(),
to: to.toISOString(),
} as { from: string; to: string; stdout?: string; stderr?: string };
if (streamConfig.stdout) {
params.stdout = "1";
}
if (streamConfig.stderr) {
params.stderr = "1";
}
const logs = await (
await fetch(`${config.base}/api/logs?id=${container.value.id}&from=${from.toISOString()}&to=${to.toISOString()}`)
await fetch(
`${config.base}/api/logs/${container.value.host}/${container.value.id}?${new URLSearchParams(
params,
).toString()}`,
)
).text();
if (logs) {
const newMessages = logs
@@ -106,7 +149,7 @@ export function useLogStream(container: ComputedRef<Container>) {
buffer.push(new DockerEventLogEntry("Container started", new Date(), "container-started"));
connect({ clear: false });
}
}
},
);
onUnmounted(() => {
@@ -118,8 +161,10 @@ export function useLogStream(container: ComputedRef<Container>) {
watch(
() => container.value.id,
() => connect(),
{ immediate: true }
{ immediate: true },
);
watch(streamConfig, () => connect());
return { ...$$({ messages }), loadOlderLogs };
}
+13
View File
@@ -0,0 +1,13 @@
const show = ref(false);
const debouncedShow = debouncedRef(show, 1000);
const delayedShow = computed({
set(newVal: boolean) {
show.value = newVal;
},
get() {
return debouncedShow.value;
},
});
export const globalShowPopup = () => delayedShow;
+9 -5
View File
@@ -1,5 +1,6 @@
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);
@@ -11,6 +12,9 @@ function matchRecord(record: Record<string, any>, regex: RegExp): boolean {
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;
}
@@ -19,9 +23,9 @@ function matchRecord(record: Record<string, any>, regex: RegExp): boolean {
}
export function useSearchFilter() {
const regex = computed(() => {
const regex = $computed(() => {
const isSmartCase = debouncedSearchFilter.value === debouncedSearchFilter.value.toLowerCase();
return isSmartCase ? new RegExp(debouncedSearchFilter.value, "i") : new RegExp(debouncedSearchFilter.value);
return new RegExp(encodeXML(debouncedSearchFilter.value), isSmartCase ? "i" : undefined);
});
function filteredMessages(messages: Ref<LogEntry<string | JSONObject>[]>) {
@@ -30,9 +34,9 @@ export function useSearchFilter() {
try {
return messages.value.filter((d) => {
if (d instanceof SimpleLogEntry) {
return regex.value.test(d.message);
return regex.test(d.message);
} else if (d instanceof ComplexLogEntry) {
return matchRecord(d.message, regex.value);
return matchRecord(d.message, regex);
}
return false;
});
@@ -59,7 +63,7 @@ export function useSearchFilter() {
return log.map((d) => markSearch(d));
}
return log.toString().replace(regex.value, (match) => `<mark>${match}</mark>`);
return log.toString().replace(regex, (match) => `<mark>${match}</mark>`);
}
function resetSearch() {
+7 -53
View File
@@ -1,3 +1,4 @@
import { toRefs } from "@vueuse/core";
const DOZZLE_SETTINGS_KEY = "DOZZLE_SETTINGS";
export const DEFAULT_SETTINGS: {
@@ -6,6 +7,7 @@ export const DEFAULT_SETTINGS: {
menuWidth: number;
smallerScrollbars: boolean;
showTimestamp: boolean;
showStd: boolean;
showAllContainers: boolean;
lightTheme: "auto" | "dark" | "light";
hourStyle: "auto" | "24" | "12";
@@ -17,6 +19,7 @@ export const DEFAULT_SETTINGS: {
menuWidth: 15,
smallerScrollbars: false,
showTimestamp: true,
showStd: false,
showAllContainers: false,
lightTheme: "auto",
hourStyle: "auto",
@@ -24,68 +27,19 @@ export const DEFAULT_SETTINGS: {
collapseNav: false,
};
const settings = useStorage(DOZZLE_SETTINGS_KEY, DEFAULT_SETTINGS);
export const settings = useStorage(DOZZLE_SETTINGS_KEY, DEFAULT_SETTINGS);
settings.value = { ...DEFAULT_SETTINGS, ...settings.value };
const search = computed({
get: () => settings.value.search,
set: (value) => (settings.value.search = value),
});
const size = computed({
get: () => settings.value.size,
set: (value) => (settings.value.size = value),
});
const menuWidth = computed({
get: () => settings.value.menuWidth,
set: (value) => (settings.value.menuWidth = value),
});
const smallerScrollbars = computed({
get: () => settings.value.smallerScrollbars,
set: (value) => (settings.value.smallerScrollbars = value),
});
const showTimestamp = computed({
get: () => settings.value.showTimestamp,
set: (value) => (settings.value.showTimestamp = value),
});
const showAllContainers = computed({
get: () => settings.value.showAllContainers,
set: (value) => (settings.value.showAllContainers = value),
});
const lightTheme = computed({
get: () => settings.value.lightTheme,
set: (value) => (settings.value.lightTheme = value),
});
const hourStyle = computed({
get: () => settings.value.hourStyle,
set: (value) => (settings.value.hourStyle = value),
});
const softWrap = computed({
get: () => settings.value.softWrap,
set: (value) => (settings.value.softWrap = value),
});
const collapseNav = computed({
get: () => settings.value.collapseNav,
set: (value) => (settings.value.collapseNav = value),
});
export {
export const {
collapseNav,
softWrap,
hourStyle,
lightTheme,
showAllContainers,
showTimestamp,
showStd,
smallerScrollbars,
menuWidth,
size,
search,
settings
};
} = toRefs(settings);
+23
View File
@@ -0,0 +1,23 @@
import { Container } from "@/models/Container";
const DOZZLE_HOST = "DOZZLE_HOST";
export const sessionHost = useSessionStorage<string | null>(DOZZLE_HOST, null);
if (config.hosts.length === 1 && !sessionHost.value) {
sessionHost.value = config.hosts[0].id;
}
export function persistentVisibleKeys(container: ComputedRef<Container>) {
return computed(() => useStorage(container.value.storageKey, []));
}
const DOZZLE_PINNED_CONTAINERS = "DOZZLE_PINNED_CONTAINERS";
export const pinnedContainers = useStorage(DOZZLE_PINNED_CONTAINERS, new Set<string>());
export function togglePinnedContainer(id: string) {
if (pinnedContainers.value.has(id)) {
pinnedContainers.value.delete(id);
} else {
pinnedContainers.value.add(id);
}
}
+6 -4
View File
@@ -3,7 +3,7 @@
<mobile-menu v-if="isMobile" @search="showFuzzySearch"></mobile-menu>
<splitpanes @resized="onResized($event)">
<pane min-size="10" :size="menuWidth" v-if="!isMobile && !collapseNav">
<side-menu @search="showFuzzySearch"></side-menu>
<side-panel @search="showFuzzySearch"></side-panel>
</pane>
<pane min-size="10">
<splitpanes>
@@ -32,10 +32,10 @@
v-if="!isMobile"
>
<span class="icon ml-2" v-if="collapseNav">
<mdi-light-chevron-right />
<mdi:light-chevron-right />
</span>
<span class="icon" v-else>
<mdi-light-chevron-left />
<mdi:light-chevron-left />
</span>
</button>
</main>
@@ -58,7 +58,7 @@ watchEffect(() => {
});
onKeyStroke("k", (e) => {
if (e.ctrlKey || e.metaKey) {
if ((e.ctrlKey || e.metaKey) && !e.shiftKey) {
showFuzzySearch();
e.preventDefault();
}
@@ -87,6 +87,7 @@ function onResized(e: any) {
:deep(.splitpanes--vertical > .splitpanes__splitter) {
min-width: 3px;
background: var(--border-color);
&:hover {
background: var(--border-hover-color);
}
@@ -110,6 +111,7 @@ function onResized(e: any) {
position: fixed;
left: 10px;
bottom: 10px;
&.collapsed {
left: -40px;
width: 60px;
+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");
+19
View File
@@ -0,0 +1,19 @@
import { describe, expect, test } from "vitest";
import { Container } from "./Container";
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);
});
});
+42 -9
View File
@@ -1,31 +1,64 @@
import type { ContainerStat, ContainerState } from "@/types/Container";
import type { ContainerHealth, ContainerStat, ContainerState } from "@/types/Container";
import type { UseThrottledRefHistoryReturn } from "@vueuse/core";
import { useExponentialMovingAverage } from "@/utils";
import { Ref } from "vue";
type Stat = Omit<ContainerStat, "id">;
const SWARM_ID_REGEX = /(\.[a-z0-9]{25})+$/i;
export class Container {
public stat: Ref<Stat>;
private _stat: Ref<Stat>;
private readonly throttledStatHistory: UseThrottledRefHistoryReturn<Stat, Stat>;
public readonly swarmId: string | null = null;
public readonly isSwarm: boolean = false;
private readonly movingAverageStat: Ref<Stat>;
constructor(
public readonly id: string,
public readonly created: number,
public readonly created: Date,
public readonly image: string,
public readonly name: string,
public readonly command: string,
public readonly host: string,
public status: string,
public state: ContainerState
public state: ContainerState,
public health?: ContainerHealth,
) {
this.stat = ref({ cpu: 0, memory: 0, memoryUsage: 0 });
this.throttledStatHistory = useThrottledRefHistory(this.stat, { capacity: 300, deep: true, throttle: 1000 });
this._stat = ref({ cpu: 0, memory: 0, memoryUsage: 0 });
this.throttledStatHistory = useThrottledRefHistory(this._stat, { capacity: 300, deep: true, throttle: 1000 });
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;
}
}
public getStatHistory() {
get statHistory() {
return unref(this.throttledStatHistory.history);
}
public getLastStat() {
return unref(this.throttledStatHistory.last);
get movingAverage() {
return unref(this.movingAverageStat);
}
get stat() {
return unref(this._stat);
}
get storageKey() {
return `${stripVersion(this.image)}:${this.command}`;
}
public updateStat(stat: Stat) {
if (isRef(this._stat)) {
this._stat.value = stat;
} else {
// @ts-ignore
this._stat = stat;
}
}
}
+59 -11
View File
@@ -11,16 +11,26 @@ export interface HasComponent {
export type JSONValue = string | number | boolean | JSONObject | Array<JSONValue>;
export type JSONObject = { [x: string]: JSONValue };
export type Position = "start" | "end" | "middle" | undefined;
export type Std = "stdout" | "stderr";
export interface LogEvent {
readonly m: string | JSONObject;
readonly ts: number;
readonly id: number;
readonly l: string;
readonly p: Position;
readonly s: "stdout" | "stderr" | "unknown";
}
export abstract class LogEntry<T extends string | JSONObject> implements HasComponent {
protected readonly _message: T;
constructor(message: T, public readonly id: number, public readonly date: Date) {
constructor(
message: T,
public readonly id: number,
public readonly date: Date,
public readonly std: Std,
public readonly level?: string,
) {
this._message = message;
}
@@ -32,6 +42,16 @@ export abstract class LogEntry<T extends string | JSONObject> implements HasComp
}
export class SimpleLogEntry extends LogEntry<string> {
constructor(
message: string,
id: number,
date: Date,
public readonly level: string,
public readonly position: Position,
public readonly std: Std,
) {
super(message, id, date, std, level);
}
getComponent(): Component {
return SimpleLogItem;
}
@@ -40,8 +60,15 @@ export class SimpleLogEntry extends LogEntry<string> {
export class ComplexLogEntry extends LogEntry<JSONObject> {
private readonly filteredMessage: ComputedRef<JSONObject>;
constructor(message: JSONObject, id: number, date: Date, visibleKeys?: Ref<string[][]>) {
super(message, id, date);
constructor(
message: JSONObject,
id: number,
date: Date,
public readonly level: string,
public readonly std: Std,
visibleKeys?: Ref<string[][]>,
) {
super(message, id, date, std, level);
if (visibleKeys) {
this.filteredMessage = computed(() => {
if (!visibleKeys.value.length) {
@@ -67,13 +94,17 @@ export class ComplexLogEntry extends LogEntry<JSONObject> {
}
static fromLogEvent(event: ComplexLogEntry, visibleKeys: Ref<string[][]>): ComplexLogEntry {
return new ComplexLogEntry(event._message, event.id, event.date, visibleKeys);
return new ComplexLogEntry(event._message, event.id, event.date, event.level, event.std, visibleKeys);
}
}
export class DockerEventLogEntry extends LogEntry<string> {
constructor(message: string, date: Date, public readonly event: string) {
super(message, date.getTime(), date);
constructor(
message: string,
date: Date,
public readonly event: string,
) {
super(message, date.getTime(), date, "stderr", "info");
}
getComponent(): Component {
return DockerEventLogItem;
@@ -88,9 +119,9 @@ export class SkippedLogsEntry extends LogEntry<string> {
date: Date,
totalSkipped: number,
public readonly firstSkipped: LogEntry<string | JSONObject>,
lastSkipped: LogEntry<string | JSONObject>
lastSkipped: LogEntry<string | JSONObject>,
) {
super("", date.getTime(), date);
super("", date.getTime(), date, "stderr", "info");
this._totalSkipped = totalSkipped;
this.lastSkipped = lastSkipped;
}
@@ -110,12 +141,29 @@ export class SkippedLogsEntry extends LogEntry<string> {
public get totalSkipped(): number {
return this._totalSkipped;
}
public get lastSkippedItem(): LogEntry<string | JSONObject> {
return this.lastSkipped;
}
}
export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
if (typeof event.m === "string") {
return new SimpleLogEntry(event.m, event.id, new Date(event.ts));
return new SimpleLogEntry(
event.m,
event.id,
new Date(event.ts),
event.l,
event.p,
event.s === "unknown" ? "stderr" : event.s ?? "stderr",
);
} else {
return new ComplexLogEntry(event.m, event.id, new Date(event.ts));
return new ComplexLogEntry(
event.m,
event.id,
new Date(event.ts),
event.l,
event.s === "unknown" ? "stderr" : event.s ?? "stderr",
);
}
}
+2 -2
View File
@@ -7,8 +7,8 @@ export const install = (app: App) => {
([key, value]) => {
const yaml = key.endsWith(".yaml");
return [key.slice(14, yaml ? -5 : -4), value.default];
}
)
},
),
);
const i18n = createI18n({
legacy: false,
@@ -1,16 +1,5 @@
import { type App } from "vue";
import {
Autocomplete,
Button,
Dropdown,
Switch,
Radio,
Skeleton,
Field,
Tooltip,
Modal,
Config,
} from "@oruga-ui/oruga-next";
import { Autocomplete, Button, Dropdown, Switch, Skeleton, Field, Table, Modal, Config } from "@oruga-ui/oruga-next";
import { bulmaConfig } from "@oruga-ui/theme-bulma";
export const install = (app: App) => {
@@ -19,10 +8,9 @@ export const install = (app: App) => {
.use(Button)
.use(Dropdown)
.use(Switch)
.use(Tooltip)
.use(Modal)
.use(Radio)
.use(Field)
.use(Skeleton)
.use(Table)
.use(Config, bulmaConfig);
};
+1 -2
View File
@@ -1,8 +1,7 @@
import { type App } from "vue";
import { createPinia } from "pinia";
export const install = (app:App) => {
export const install = (app: App) => {
const pinia = createPinia();
app.use(pinia);
};
+8 -2
View File
@@ -1,14 +1,20 @@
<template>
<search></search>
<log-container :id="id" show-title :scrollable="activeContainers.length > 0"> </log-container>
<log-container :id="id" show-title :scrollable="activeContainers.length > 0" v-if="currentContainer"></log-container>
<div v-else-if="ready" class="notification is-warning is-light m-6">
<h1 class="title">
{{ $t("error.container-not-found") }}
</h1>
</div>
</template>
<script lang="ts" setup>
import search from "@/components/Search.vue";
const store = useContainerStore();
const { id } = defineProps<{ id: string }>();
const currentContainer = store.currentContainer($$(id));
const { activeContainers } = storeToRefs(store);
const { activeContainers, ready } = storeToRefs(store);
setTitle("loading");
+72 -118
View File
@@ -1,163 +1,100 @@
<template>
<div>
<section class="level section pb-0-is-mobile">
<div class="level-item has-text-centered">
<div>
<p class="title">{{ containers.length }}</p>
<p class="heading">{{ $t("label.total-containers") }}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title">{{ runningContainers.length }}</p>
<p class="heading">{{ $t("label.running") }}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title" data-ci-skip>{{ totalCpu }}%</p>
<p class="heading">{{ $t("label.total-cpu-usage") }}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title" data-ci-skip>{{ formatBytes(totalMem) }}</p>
<p class="heading">{{ $t("label.total-mem-usage") }}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title">{{ version }}</p>
<p class="heading">{{ $t("label.dozzle-version") }}</p>
</div>
</div>
</section>
<section class="columns is-centered section is-marginless pt-0-is-mobile">
<div class="column is-12-mobile is-6-tablet is-5-desktop is-4-fullhd">
<div class="panel">
<p class="panel-heading">{{ $t("label.containers") }}</p>
<div class="panel-block">
<p class="control has-icons-left">
<input
class="input"
type="text"
:placeholder="$t('placeholder.search-containers')"
v-model="query"
@keyup.esc="query = ''"
@keyup.enter="onEnter()"
/>
<span class="icon is-left">
<search-icon />
</span>
</p>
<div class="section tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child box">
<div class="level-item has-text-centered">
<div>
<p class="title">{{ runningContainers.length }} / {{ containers.length }}</p>
<p class="heading">{{ $t("label.running") }} / {{ $t("label.total-containers") }}</p>
</div>
<p class="panel-tabs" v-if="query === ''">
<a :class="{ 'is-active': sort === 'running' }" @click="sort = 'running'">{{ $t("label.running") }}</a>
<a :class="{ 'is-active': sort === 'all' }" @click="sort = 'all'">{{ $t("label.all") }}</a>
</p>
<router-link
:to="{ name: 'container-id', params: { id: item.id } }"
v-for="item in data.slice(0, 10)"
:key="item.id"
class="panel-block"
>
<span class="name">{{ item.name }}</span>
<div class="subtitle is-7 status">
<past-time :date="new Date(item.created * 1000)"></past-time>
</div>
</router-link>
</div>
</div>
</section>
</div>
<div class="tile is-parent">
<div class="tile is-child box">
<div class="level-item has-text-centered">
<div>
<p class="title">{{ totalCpu }}%</p>
<p class="heading">{{ $t("label.total-cpu-usage") }}</p>
</div>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child box">
<div class="level-item has-text-centered">
<div>
<p class="title">{{ formatBytes(totalMem) }}</p>
<p class="heading">{{ $t("label.total-mem-usage") }}</p>
</div>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child box">
<div class="level-item has-text-centered">
<div>
<p class="title">{{ version }}</p>
<p class="heading">{{ $t("label.dozzle-version") }}</p>
</div>
</div>
</div>
</div>
</div>
<section class="section table-container">
<div class="box">
<container-table :containers="runningContainers"></container-table>
</div>
</section>
</template>
<script lang="ts" setup>
import SearchIcon from "~icons/mdi-light/magnify";
import { useFuse } from "@vueuse/integrations/useFuse";
import { Container } from "@/models/Container";
const { version } = config;
const containerStore = useContainerStore();
const { containers } = storeToRefs(containerStore);
const router = useRouter();
const { containers } = storeToRefs(containerStore) as { containers: unknown } as { containers: Ref<Container[]> };
const sort = $ref("running");
const query = ref("");
const mostRecentContainers = $computed(() => [...containers.value].sort((a, b) => b.created - a.created));
const mostRecentContainers = $computed(() => [...containers.value].sort((a, b) => +b.created - +a.created));
const runningContainers = $computed(() => mostRecentContainers.filter((c) => c.state === "running"));
const list = computed(() => {
return containers.value.map(({ id, created, name, state }) => {
return {
id,
created,
name,
state,
};
});
});
const { results } = useFuse(query, list, {
fuseOptions: { keys: ["name"] },
matchAllWhenSearchEmpty: false,
});
const data = computed(() => {
if (results.value.length) {
return results.value.map(({ item }) => item);
}
switch (sort) {
case "all":
return mostRecentContainers;
case "running":
return runningContainers;
default:
throw `Invalid sort order: ${sort}`;
}
});
let totalCpu = $ref(0);
useIntervalFn(
() => {
totalCpu = runningContainers.reduce((acc, c) => acc + (c.stat?.cpu ?? 0), 0);
totalCpu = runningContainers.reduce((acc, c) => acc + c.stat.cpu, 0);
},
1000,
{ immediate: true }
{ immediate: true },
);
let totalMem = $ref(0);
useIntervalFn(
() => {
totalMem = runningContainers.reduce((acc, c) => acc + (c.stat?.memoryUsage ?? 0), 0);
totalMem = runningContainers.reduce((acc, c) => acc + c.stat.memoryUsage, 0);
},
1000,
{ immediate: true }
{ immediate: true },
);
function onEnter() {
if (data.value.length > 0) {
const item = data.value[0];
router.push({ name: "container-id", params: { id: item.id } });
}
}
</script>
<style lang="scss" scoped>
.panel {
border: 1px solid var(--border-color);
.panel-block,
.panel-tabs {
border-color: var(--border-color);
.is-active {
border-color: var(--border-hover-color);
}
.name {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.status {
margin-left: auto;
white-space: nowrap;
@@ -178,4 +115,21 @@ function onEnter() {
.icon {
padding: 10px 3px;
}
.bar-chart {
height: 1.5em;
.bar-text {
font-size: 0.9em;
padding: 0 0.5em;
}
}
:deep(tr td) {
padding-top: 1em;
padding-bottom: 1em;
}
.section + .section {
padding-top: 0;
}
</style>
+1 -1
View File
@@ -57,7 +57,7 @@ setTitle(t("title.login"));
let error = $ref(false);
let username = $ref("");
let password = $ref("");
let form: HTMLFormElement = $ref();
let form: HTMLFormElement | undefined = $ref();
async function onLogin() {
const response = await fetch(`${config.base}/api/validateCredentials`, {
+12 -7
View File
@@ -25,6 +25,9 @@
<div class="item">
<o-switch v-model="showTimestamp"> {{ $t("settings.show-timesamps") }} </o-switch>
</div>
<div class="item">
<o-switch v-model="showStd"> {{ $t("settings.show-std") }} </o-switch>
</div>
<div class="item">
<o-switch v-model="softWrap"> {{ $t("settings.soft-wrap") }}</o-switch>
@@ -39,7 +42,7 @@
<o-button variant="primary" type="button">
<span class="is-capitalized">{{ hourStyle }}</span>
<span class="icon">
<carbon-caret-down />
<carbon:caret-down />
</span>
</o-button>
</template>
@@ -64,7 +67,7 @@
<o-button variant="primary" type="button">
<span class="is-capitalized">{{ size }}</span>
<span class="icon">
<carbon-caret-down />
<carbon:caret-down />
</span>
</o-button>
</template>
@@ -92,7 +95,7 @@
<o-button variant="primary" type="button">
<span class="is-capitalized">{{ lightTheme }}</span>
<span class="icon">
<carbon-caret-down />
<carbon:caret-down />
</span>
</o-button>
</template>
@@ -119,7 +122,7 @@
<div class="item">
<o-switch v-model="search">
<span v-html="$t('settings.search')"></span>
<span>{{ $t("settings.search") }} <key-shortcut char="f"></key-shortcut></span>
</o-switch>
</div>
@@ -131,12 +134,12 @@
</template>
<script lang="ts" setup>
import gt from "semver/functions/gt";
import {
search,
lightTheme,
smallerScrollbars,
showTimestamp,
showStd,
hourStyle,
showAllContainers,
size,
@@ -147,7 +150,7 @@ const { t } = useI18n();
setTitle(t("title.settings"));
const currentVersion = $ref(config.version);
const currentVersion = config.version;
let nextRelease = $ref({ html_url: "", name: "" });
let hasUpdate = $ref(false);
@@ -156,7 +159,8 @@ async function fetchNextRelease() {
const response = await fetch("https://api.github.com/repos/amir20/dozzle/releases/latest");
if (response.ok) {
const release = await response.json();
hasUpdate = gt(release.tag_name, currentVersion);
hasUpdate =
release.tag_name.slice(1).localeCompare(currentVersion, undefined, { numeric: true, sensitivity: "base" }) > 0;
nextRelease = release;
}
} else {
@@ -177,6 +181,7 @@ fetchNextRelease();
a.next-release {
text-decoration: underline;
&:hover {
text-decoration: none;
}
+5 -14
View File
@@ -3,10 +3,11 @@ const text = document.querySelector("script#config__json")?.textContent || "{}";
interface Config {
version: string;
base: string;
authorizationNeeded: boolean | "false" | "true";
secured: boolean | "false" | "true";
authorizationNeeded: boolean;
secured: boolean;
maxLogs: number;
hostname: string;
hosts: { name: string; id: string }[];
}
const pageConfig = JSON.parse(text);
@@ -16,16 +17,6 @@ const config: Config = {
...pageConfig,
};
if (config.version == "{{ .Version }}") {
config.version = "master";
config.base = "";
config.authorizationNeeded = false;
config.secured = false;
config.hostname = "localhost";
} else {
config.version = config.version.replace(/^v/, "");
config.authorizationNeeded = config.authorizationNeeded === "true";
config.secured = config.secured === "true";
}
config.version = config.version.replace(/^v/, "");
export default config as Config;
export default config;
+71 -35
View File
@@ -1,17 +1,22 @@
import { acceptHMRUpdate, defineStore } from "pinia";
import { Ref, UnwrapNestedRefs } from "vue";
import type { ContainerJson, ContainerStat } from "@/types/Container";
import type { ContainerHealth, ContainerJson, ContainerStat } from "@/types/Container";
import { Container } from "@/models/Container";
export const useContainerStore = defineStore("container", () => {
const containers: Ref<Container[]> = ref([]);
const activeContainerIds: Ref<string[]> = ref([]);
let es: EventSource | null = null;
const ready = ref(false);
const allContainersById = computed(() =>
containers.value.reduce((acc, container) => {
acc[container.id] = container;
return acc;
}, {} as Record<string, Container>)
containers.value.reduce(
(acc, container) => {
acc[container.id] = container;
return acc;
},
{} as Record<string, Container>,
),
);
const visibleContainers = computed(() => {
@@ -21,36 +26,70 @@ export const useContainerStore = defineStore("container", () => {
const activeContainers = computed(() => activeContainerIds.value.map((id) => allContainersById.value[id]));
const es = new EventSource(`${config.base}/api/events/stream`);
es.addEventListener("containers-changed", (e: Event) =>
setContainers(JSON.parse((e as MessageEvent).data) as ContainerJson[])
);
es.addEventListener("container-stat", (e) => {
const stat = JSON.parse((e as MessageEvent).data) as ContainerStat;
const container = allContainersById.value[stat.id] as unknown as UnwrapNestedRefs<Container>;
if (container) {
const { id, ...rest } = stat;
container.stat = rest;
}
});
es.addEventListener("container-die", (e) => {
const event = JSON.parse((e as MessageEvent).data) as { actorId: string };
const container = allContainersById.value[event.actorId];
if (container) {
container.state = "dead";
}
});
function connect() {
es?.close();
ready.value = false;
es = new EventSource(`${config.base}/api/events/stream`);
const setContainers = (newContainers: ContainerJson[]) => {
containers.value = newContainers.map((c) => {
const existing = allContainersById.value[c.id];
if (existing) {
existing.status = c.status;
existing.state = c.state;
return existing;
es.addEventListener("containers-changed", (e: Event) =>
updateContainers(JSON.parse((e as MessageEvent).data) as ContainerJson[]),
);
es.addEventListener("container-stat", (e) => {
const stat = JSON.parse((e as MessageEvent).data) as ContainerStat;
const container = allContainersById.value[stat.id] as unknown as UnwrapNestedRefs<Container>;
if (container) {
const { id, ...rest } = stat;
container.updateStat(rest);
}
return new Container(c.id, c.created, c.image, c.name, c.command, c.status, c.state);
});
es.addEventListener("container-die", (e) => {
const event = JSON.parse((e as MessageEvent).data) as { actorId: string };
const container = allContainersById.value[event.actorId];
if (container) {
container.state = "dead";
}
});
es.addEventListener("container-health", (e) => {
const event = JSON.parse((e as MessageEvent).data) as { actorId: string; health: ContainerHealth };
const container = allContainersById.value[event.actorId];
if (container) {
container.health = event.health;
}
});
watchOnce(containers, () => (ready.value = true));
}
connect();
const updateContainers = (containersPayload: ContainerJson[]) => {
const existingContainers = containersPayload.filter((c) => allContainersById.value[c.id]);
const newContainers = containersPayload.filter((c) => !allContainersById.value[c.id]);
existingContainers.forEach((c) => {
const existing = allContainersById.value[c.id];
existing.status = c.status;
existing.state = c.state;
existing.health = c.health;
});
containers.value = [
...containers.value,
...newContainers.map((c) => {
return new Container(
c.id,
new Date(c.created * 1000),
c.image,
c.name,
c.command,
c.host,
c.status,
c.state,
c.health,
);
}),
];
};
const currentContainer = (id: Ref<string>) => computed(() => allContainersById.value[id.value]);
@@ -58,9 +97,6 @@ export const useContainerStore = defineStore("container", () => {
const removeActiveContainer = ({ id }: Container) =>
activeContainerIds.value.splice(activeContainerIds.value.indexOf(id), 1);
const ready = ref(false);
watchOnce(containers, () => (ready.value = true));
return {
containers,
activeContainerIds,
+40 -3
View File
@@ -24,17 +24,17 @@ $panel-heading-color: var(--panel-heading-color);
$link: $turquoise;
$link-active: $grey-dark;
$link-hover: $yellow;
$dark-toolbar-color: rgba($black-bis, 0.7);
$light-toolbar-color: rgba($grey-darker, 0.7);
@import "bulma/bulma.sass";
@import "bulma/bulma";
@import "@oruga-ui/theme-bulma/dist/scss/components/utils/all.scss";
@import "@oruga-ui/theme-bulma/dist/scss/components/autocomplete.scss";
@import "@oruga-ui/theme-bulma/dist/scss/components/button.scss";
@import "@oruga-ui/theme-bulma/dist/scss/components/modal.scss";
@import "@oruga-ui/theme-bulma/dist/scss/components/switch.scss";
@import "@oruga-ui/theme-bulma/dist/scss/components/tooltip.scss";
@import "@oruga-ui/theme-bulma/dist/scss/components/dropdown.scss";
@import "@oruga-ui/theme-bulma/dist/scss/components/skeleton.scss";
@import "splitpanes/dist/splitpanes.css";
@@ -117,6 +117,34 @@ $light-toolbar-color: rgba($grey-darker, 0.7);
}
}
:root {
--green-color: #00b5ad;
--red-color: #f44336;
--purple-color: #9c27b0;
--orange-color: #ff9800;
--blue-color: #2196f3;
}
.is-red {
color: var(--red-color);
}
.is-green {
color: var(--green-color);
}
.is-purple {
color: var(--purple-color);
}
.is-orange {
color: var(--orange-color);
}
.is-blue {
color: var(--blue-color);
}
html {
overflow-x: unset;
overflow-y: unset;
@@ -177,6 +205,9 @@ html.has-custom-scrollbars {
.modal {
z-index: 1000;
.modal-background {
backdrop-filter: blur(10px);
}
}
.button .button-wrapper > span {
@@ -188,7 +219,9 @@ html.has-custom-scrollbars {
}
.has-boxshadow {
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
box-shadow:
0 1px 3px 0 rgb(0 0 0 / 0.1),
0 1px 2px -1px rgb(0 0 0 / 0.1);
}
mark {
@@ -211,3 +244,7 @@ mark {
color: var(--text-strong-color);
background: var(--scheme-main-ter);
}
a {
transition: color 0.2s ease-in-out;
}
+3
View File
@@ -13,6 +13,9 @@ export type ContainerJson = {
readonly command: string;
readonly status: string;
readonly state: ContainerState;
readonly host: string;
readonly health?: ContainerHealth;
};
export type ContainerState = "created" | "running" | "exited" | "dead" | "paused" | "restarting";
export type ContainerHealth = "healthy" | "unhealthy" | "starting";
+14 -8
View File
@@ -1,7 +1,3 @@
import { Container } from "@/models/Container";
import { useStorage } from "@vueuse/core";
import { computed, ComputedRef } from "vue";
export function formatBytes(bytes: number, decimals = 2) {
if (bytes === 0) return "0 Bytes";
const k = 1024;
@@ -37,11 +33,21 @@ export function arrayEquals(a: string[], b: string[]): boolean {
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((val, index) => val === b[index]);
}
export function persistentVisibleKeys(container: ComputedRef<Container>) {
return computed(() => useStorage(stripVersion(container.value.image) + ":" + container.value.command, []));
}
export function stripVersion(label: string) {
const [name, _] = label.split(":");
return name;
}
export function useExponentialMovingAverage<T extends Record<string, number>>(source: Ref<T>, alpha: number = 0.2) {
const ema = ref<T>(source.value) as Ref<T>;
watch(source, (value) => {
const newValue = {} as Record<string, number>;
for (const key in value) {
newValue[key] = alpha * value[key] + (1 - alpha) * ema.value[key];
}
ema.value = newValue as T;
});
return ema;
}
+76
View File
@@ -0,0 +1,76 @@
version: "3.4"
services:
custom_base:
container_name: custom_base
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- DOZZLE_FILTER=name=custom_base
- DOZZLE_BASE=/foobarbase
- DOZZLE_NO_ANALYTICS=1
ports:
- 8080:8080
build:
context: .
auth:
container_name: auth
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- DOZZLE_FILTER=name=auth
- DOZZLE_USERNAME=foo
- DOZZLE_PASSWORD=bar
- DOZZLE_NO_ANALYTICS=1
ports:
- 9090:8080
build:
context: .
dozzle:
container_name: dozzle
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- DOZZLE_FILTER=name=dozzle
- DOZZLE_NO_ANALYTICS=1
ports:
- 7070:8080
build:
context: .
remote:
container_name: remote
environment:
- DOZZLE_REMOTE_HOST=tcp://proxy:2375
- DOZZLE_FILTER=name=dozzle
- DOZZLE_NO_ANALYTICS=1
ports:
- 5050:8080
build:
context: .
depends_on:
- proxy
proxy:
container_name: proxy
image: tecnativa/docker-socket-proxy
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- CONTAINERS=1
ports:
- 2375:2375
playwright:
container_name: playwright
image: mcr.microsoft.com/playwright:v1.37.1-jammy
working_dir: /app
volumes:
- .:/app
command: npx --yes playwright test
environment:
- PWTEST_SKIP_TEST_OUTPUT=1
- CI=1
depends_on:
- dozzle
- custom_base
- auth
- remote
+108 -45
View File
@@ -5,6 +5,8 @@ import (
"encoding/json"
"fmt"
"io"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
@@ -14,15 +16,33 @@ import (
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
log "github.com/sirupsen/logrus"
)
type dockerClient struct {
cli dockerProxy
filters filters.Args
type StdType int
const (
UNKNOWN StdType = 1 << iota
STDOUT
STDERR
)
const STDALL = STDOUT | STDERR
func (s StdType) String() string {
switch s {
case STDOUT:
return "stdout"
case STDERR:
return "stderr"
case STDALL:
return "all"
default:
return "unknown"
}
}
type dockerProxy interface {
type DockerCLI interface {
ContainerList(context.Context, types.ContainerListOptions) ([]types.Container, error)
ContainerLogs(context.Context, string, types.ContainerLogsOptions) (io.ReadCloser, error)
Events(context.Context, types.EventsOptions) (<-chan events.Message, <-chan error)
@@ -31,19 +51,18 @@ type dockerProxy interface {
Ping(ctx context.Context) (types.Ping, error)
}
// Client is a proxy around the docker client
type Client interface {
ListContainers() ([]Container, error)
FindContainer(string) (Container, error)
ContainerLogs(context.Context, string, string) (io.ReadCloser, error)
Events(context.Context) (<-chan ContainerEvent, <-chan error)
ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time) (io.ReadCloser, error)
ContainerStats(context.Context, string, chan<- ContainerStat) error
Ping(context.Context) (types.Ping, error)
type Client struct {
cli DockerCLI
filters filters.Args
host *Host
}
func NewClient(cli DockerCLI, filters filters.Args, host *Host) *Client {
return &Client{cli, filters, host}
}
// NewClientWithFilters creates a new instance of Client with docker filters
func NewClientWithFilters(f map[string][]string) Client {
func NewClientWithFilters(f map[string][]string) (*Client, error) {
filterArgs := filters.NewArgs()
for key, values := range f {
for _, value := range values {
@@ -56,13 +75,49 @@ func NewClientWithFilters(f map[string][]string) Client {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
log.Fatal(err)
return nil, err
}
return &dockerClient{cli, filterArgs}
return NewClient(cli, filterArgs, &Host{Name: "localhost", ID: "localhost"}), nil
}
func (d *dockerClient) FindContainer(id string) (Container, error) {
func NewClientWithTlsAndFilter(f map[string][]string, host Host) (*Client, error) {
filterArgs := filters.NewArgs()
for key, values := range f {
for _, value := range values {
filterArgs.Add(key, value)
}
}
log.Debugf("filterArgs = %v", filterArgs)
if host.URL.Scheme != "tcp" {
log.Fatal("Only tcp scheme is supported")
}
opts := []client.Opt{
client.WithHost(host.URL.String()),
}
if host.ValidCerts {
log.Debugf("Using TLS client config with certs at: %s", filepath.Dir(host.CertPath))
opts = append(opts, client.WithTLSClientConfig(host.CACertPath, host.CertPath, host.KeyPath))
} else {
log.Debugf("No valid certs found, using plain TCP")
}
opts = append(opts, client.WithAPIVersionNegotiation())
cli, err := client.NewClientWithOpts(opts...)
if err != nil {
return nil, err
}
return NewClient(cli, filterArgs, &host), nil
}
func (d *Client) FindContainer(id string) (Container, error) {
var container Container
containers, err := d.ListContainers()
if err != nil {
@@ -81,10 +136,16 @@ func (d *dockerClient) FindContainer(id string) (Container, error) {
return container, fmt.Errorf("unable to find container with id: %s", id)
}
if json, err := d.cli.ContainerInspect(context.Background(), container.ID); err == nil {
container.Tty = json.Config.Tty
} else {
return container, err
}
return container, nil
}
func (d *dockerClient) ListContainers() ([]Container, error) {
func (d *Client) ListContainers() ([]Container, error) {
containerListOptions := types.ContainerListOptions{
Filters: d.filters,
All: true,
@@ -106,6 +167,8 @@ func (d *dockerClient) ListContainers() ([]Container, error) {
Created: c.Created,
State: c.State,
Status: c.Status,
Host: d.host.ID,
Health: findBetweenParentheses(c.Status),
}
containers = append(containers, container)
}
@@ -117,7 +180,7 @@ func (d *dockerClient) ListContainers() ([]Container, error) {
return containers, nil
}
func (d *dockerClient) ContainerStats(ctx context.Context, id string, stats chan<- ContainerStat) error {
func (d *Client) ContainerStats(ctx context.Context, id string, stats chan<- ContainerStat) error {
response, err := d.cli.ContainerStats(ctx, id, true)
if err != nil {
@@ -169,8 +232,8 @@ func (d *dockerClient) ContainerStats(ctx context.Context, id string, stats chan
return nil
}
func (d *dockerClient) ContainerLogs(ctx context.Context, id string, since string) (io.ReadCloser, error) {
log.WithField("id", id).WithField("since", since).Debug("streaming logs for container")
func (d *Client) ContainerLogs(ctx context.Context, id string, since string, stdType StdType) (io.ReadCloser, error) {
log.WithField("id", id).WithField("since", since).WithField("stdType", stdType).Debug("streaming logs for container")
if since != "" {
if millis, err := strconv.ParseInt(since, 10, 64); err == nil {
@@ -181,34 +244,26 @@ func (d *dockerClient) ContainerLogs(ctx context.Context, id string, since strin
}
options := types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
ShowStdout: stdType&STDOUT != 0,
ShowStderr: stdType&STDERR != 0,
Follow: true,
Tail: "300",
Timestamps: true,
Since: since,
}
log.Debugf("streaming logs from Docker with option: %+v", options)
reader, err := d.cli.ContainerLogs(ctx, id, options)
if err != nil {
return nil, err
}
containerJSON, err := d.cli.ContainerInspect(ctx, id)
if err != nil {
return nil, err
}
return newLogReader(reader, containerJSON.Config.Tty), nil
return reader, nil
}
func (d *dockerClient) Events(ctx context.Context) (<-chan ContainerEvent, <-chan error) {
func (d *Client) Events(ctx context.Context, messages chan<- ContainerEvent) <-chan error {
dockerMessages, errors := d.cli.Events(ctx, types.EventsOptions{})
messages := make(chan ContainerEvent)
go func() {
defer close(messages)
for {
select {
@@ -223,19 +278,20 @@ func (d *dockerClient) Events(ctx context.Context) (<-chan ContainerEvent, <-cha
messages <- ContainerEvent{
ActorID: message.Actor.ID[:12],
Name: message.Action,
Host: d.host.ID,
}
}
}
}
}()
return messages, errors
return errors
}
func (d *dockerClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time) (io.ReadCloser, error) {
func (d *Client) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType StdType) (io.ReadCloser, error) {
options := types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
ShowStdout: stdType&STDOUT != 0,
ShowStderr: stdType&STDERR != 0,
Timestamps: true,
Since: from.Format(time.RFC3339),
Until: to.Format(time.RFC3339),
@@ -244,19 +300,26 @@ func (d *dockerClient) ContainerLogsBetweenDates(ctx context.Context, id string,
log.Debugf("fetching logs from Docker with option: %+v", options)
reader, err := d.cli.ContainerLogs(ctx, id, options)
if err != nil {
return nil, err
}
containerJSON, err := d.cli.ContainerInspect(ctx, id)
if err != nil {
return nil, err
}
return newLogReader(reader, containerJSON.Config.Tty), nil
return reader, nil
}
func (d *dockerClient) Ping(ctx context.Context) (types.Ping, error) {
func (d *Client) Ping(ctx context.Context) (types.Ping, error) {
return d.cli.Ping(ctx)
}
func (d *Client) Host() *Host {
return d.host
}
var PARENTHESIS_RE = regexp.MustCompile(`\(([a-zA-Z]+)\)`)
func findBetweenParentheses(s string) string {
if results := PARENTHESIS_RE.FindStringSubmatch(s); results != nil {
return results[1]
}
return ""
}
+23 -46
View File
@@ -6,8 +6,7 @@ import (
"encoding/binary"
"errors"
"io"
"io/ioutil"
"strings"
"testing"
"github.com/docker/docker/api/types"
@@ -20,7 +19,7 @@ import (
type mockedProxy struct {
mock.Mock
dockerProxy
DockerCLI
}
func (m *mockedProxy) ContainerList(context.Context, types.ContainerListOptions) ([]types.Container, error) {
@@ -44,12 +43,7 @@ func (m *mockedProxy) ContainerLogs(ctx context.Context, id string, options type
func (m *mockedProxy) ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error) {
args := m.Called(ctx, containerID)
json, ok := args.Get(0).(types.ContainerJSON)
if !ok && args.Get(0) != nil {
panic("proxies return value is not of type types.ContainerJSON")
}
return json, args.Error(1)
return args.Get(0).(types.ContainerJSON), args.Error(1)
}
func (m *mockedProxy) ContainerStats(ctx context.Context, containerID string, stream bool) (types.ContainerStats, error) {
@@ -59,7 +53,7 @@ func (m *mockedProxy) ContainerStats(ctx context.Context, containerID string, st
func Test_dockerClient_ListContainers_null(t *testing.T) {
proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, nil)
client := &dockerClient{proxy, filters.NewArgs()}
client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
list, err := client.ListContainers()
assert.Empty(t, list, "list should be empty")
@@ -71,7 +65,7 @@ func Test_dockerClient_ListContainers_null(t *testing.T) {
func Test_dockerClient_ListContainers_error(t *testing.T) {
proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, errors.New("test"))
client := &dockerClient{proxy, filters.NewArgs()}
client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
list, err := client.ListContainers()
assert.Nil(t, list, "list should be nil")
@@ -94,7 +88,7 @@ func Test_dockerClient_ListContainers_happy(t *testing.T) {
proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
client := &dockerClient{proxy, filters.NewArgs()}
client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
list, err := client.ListContainers()
require.NoError(t, err, "error should not return an error.")
@@ -104,11 +98,13 @@ func Test_dockerClient_ListContainers_happy(t *testing.T) {
ID: "1234567890_a",
Name: "a_test_container",
Names: []string{"/a_test_container"},
Host: "localhost",
},
{
ID: "abcdefghijkl",
Name: "z_test_container",
Names: []string{"/z_test_container"},
Host: "localhost",
},
})
@@ -125,40 +121,15 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) {
binary.BigEndian.PutUint32(b[4:], uint32(len(expected)))
b = append(b, []byte(expected)...)
reader := ioutil.NopCloser(bytes.NewReader(b))
reader := io.NopCloser(bytes.NewReader(b))
options := types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: "300", Timestamps: true, Since: "since"}
proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil)
json := types.ContainerJSON{Config: &container.Config{Tty: false}}
proxy.On("ContainerInspect", mock.Anything, id).Return(json, nil)
client := &dockerClient{proxy, filters.NewArgs()}
logReader, _ := client.ContainerLogs(context.Background(), id, "since")
actual, _ := ioutil.ReadAll(logReader)
assert.Equal(t, expected, string(actual), "message doesn't match expected")
proxy.AssertExpectations(t)
}
func Test_dockerClient_ContainerLogs_happy_with_tty(t *testing.T) {
id := "123456"
proxy := new(mockedProxy)
expected := "INFO Testing logs..."
reader := ioutil.NopCloser(strings.NewReader(expected))
options := types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: "300", Timestamps: true}
proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil)
json := types.ContainerJSON{Config: &container.Config{Tty: true}}
proxy.On("ContainerInspect", mock.Anything, id).Return(json, nil)
client := &dockerClient{proxy, filters.NewArgs()}
logReader, _ := client.ContainerLogs(context.Background(), id, "")
actual, _ := ioutil.ReadAll(logReader)
assert.Equal(t, expected, string(actual), "message doesn't match expected")
client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
logReader, _ := client.ContainerLogs(context.Background(), id, "since", STDALL)
actual, _ := io.ReadAll(logReader)
assert.Equal(t, string(b), string(actual), "message doesn't match expected")
proxy.AssertExpectations(t)
}
@@ -168,9 +139,9 @@ func Test_dockerClient_ContainerLogs_error(t *testing.T) {
proxy.On("ContainerLogs", mock.Anything, id, mock.Anything).Return(nil, errors.New("test"))
client := &dockerClient{proxy, filters.NewArgs()}
client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
reader, err := client.ContainerLogs(context.Background(), id, "")
reader, err := client.ContainerLogs(context.Background(), id, "", STDALL)
assert.Nil(t, reader, "reader should be nil")
assert.Error(t, err, "error should have been returned")
@@ -191,7 +162,11 @@ func Test_dockerClient_FindContainer_happy(t *testing.T) {
proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
client := &dockerClient{proxy, filters.NewArgs()}
json := types.ContainerJSON{Config: &container.Config{Tty: false}}
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil)
client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
container, err := client.FindContainer("abcdefghijkl")
require.NoError(t, err, "error should not be thrown")
@@ -200,6 +175,8 @@ func Test_dockerClient_FindContainer_happy(t *testing.T) {
ID: "abcdefghijkl",
Name: "z_test_container",
Names: []string{"/z_test_container"},
Host: "localhost",
Tty: false,
})
proxy.AssertExpectations(t)
@@ -218,7 +195,7 @@ func Test_dockerClient_FindContainer_error(t *testing.T) {
proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
client := &dockerClient{proxy, filters.NewArgs()}
client := &Client{proxy, filters.NewArgs(), &Host{ID: "localhost"}}
_, err := client.FindContainer("not_valid")
require.Error(t, err, "error should be thrown")
+198
View File
@@ -0,0 +1,198 @@
package docker
import (
"bufio"
"bytes"
"encoding/binary"
"encoding/json"
"fmt"
"hash/fnv"
"io"
"strings"
"sync"
"time"
log "github.com/sirupsen/logrus"
)
type EventGenerator struct {
Events chan *LogEvent
Errors chan error
reader *bufio.Reader
next *LogEvent
buffer chan *LogEvent
tty bool
wg sync.WaitGroup
}
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
var BadHeaderErr = fmt.Errorf("dozzle/docker: unable to read header")
func NewEventGenerator(reader io.Reader, tty bool) *EventGenerator {
generator := &EventGenerator{
reader: bufio.NewReader(reader),
buffer: make(chan *LogEvent, 100),
Errors: make(chan error, 1),
Events: make(chan *LogEvent),
tty: tty,
}
generator.wg.Add(2)
go generator.consumeReader()
go generator.processBuffer()
return generator
}
func (g *EventGenerator) processBuffer() {
var current, next *LogEvent
for {
if g.next != nil {
current = g.next
g.next = nil
next = g.peek()
} else {
event, ok := <-g.buffer
if !ok {
close(g.Events)
break
}
current = event
next = g.peek()
}
checkPosition(current, next)
g.Events <- current
}
g.wg.Done()
}
func (g *EventGenerator) consumeReader() {
for {
message, streamType, readerError := readEvent(g.reader, g.tty)
if message != "" {
logEvent := createEvent(message, streamType)
logEvent.Level = guessLogLevel(logEvent)
g.buffer <- logEvent
}
if readerError != nil {
if readerError != BadHeaderErr {
g.Errors <- readerError
}
close(g.buffer)
break
}
}
g.wg.Done()
}
func (g *EventGenerator) peek() *LogEvent {
if g.next != nil {
return g.next
}
select {
case event := <-g.buffer:
g.next = event
return g.next
case <-time.After(50 * time.Millisecond):
return nil
}
}
func readEvent(reader *bufio.Reader, tty bool) (string, StdType, error) {
header := []byte{0, 0, 0, 0, 0, 0, 0, 0}
buffer := bufPool.Get().(*bytes.Buffer)
buffer.Reset()
defer bufPool.Put(buffer)
var streamType StdType = STDOUT
if tty {
message, err := reader.ReadString('\n')
if err != nil {
return message, streamType, err
}
return message, streamType, nil
} else {
n, err := reader.Read(header)
if err != nil {
return "", streamType, err
}
if n != 8 {
return "", streamType, BadHeaderErr
}
switch header[0] {
case 1:
streamType = STDOUT
case 2:
streamType = STDERR
default:
log.Warnf("unknown stream type %d", header[0])
}
count := binary.BigEndian.Uint32(header[4:])
if count == 0 {
return "", streamType, nil
}
_, err = io.CopyN(buffer, reader, int64(count))
if err != nil {
return "", streamType, err
}
return buffer.String(), streamType, nil
}
}
func createEvent(message string, streamType StdType) *LogEvent {
h := fnv.New32a()
h.Write([]byte(message))
logEvent := &LogEvent{Id: h.Sum32(), Message: message, Stream: streamType.String()}
if index := strings.IndexAny(message, " "); index != -1 {
logId := message[:index]
if timestamp, err := time.Parse(time.RFC3339Nano, logId); err == nil {
logEvent.Timestamp = timestamp.UnixMilli()
message = strings.TrimSuffix(message[index+1:], "\n")
logEvent.Message = message
if json.Valid([]byte(message)) {
var data map[string]interface{}
if err := json.Unmarshal([]byte(message), &data); err != nil {
log.Warnf("unable to parse json logs - error was \"%v\" while trying unmarshal \"%v\"", err.Error(), message)
} else {
logEvent.Message = data
}
}
}
}
return logEvent
}
func checkPosition(currentEvent *LogEvent, nextEvent *LogEvent) {
currentLevel := guessLogLevel(currentEvent)
if nextEvent != nil {
if currentEvent.IsCloseToTime(nextEvent) && currentLevel != "" && !nextEvent.HasLevel() {
currentEvent.Position = START
nextEvent.Position = MIDDLE
}
// If next item is not close to current item or has level, set current item position to end
if currentEvent.Position == MIDDLE && (nextEvent.HasLevel() || !currentEvent.IsCloseToTime(nextEvent)) {
currentEvent.Position = END
}
// If next item is close to current item and has no level, set next item position to middle
if currentEvent.Position == MIDDLE && !nextEvent.HasLevel() && currentEvent.IsCloseToTime(nextEvent) {
nextEvent.Position = MIDDLE
}
// Set next item level to current item level
if currentEvent.Position == START || currentEvent.Position == MIDDLE {
nextEvent.Level = currentEvent.Level
}
} else if currentEvent.Position == MIDDLE {
currentEvent.Position = END
}
}
+149
View File
@@ -0,0 +1,149 @@
package docker
import (
"bufio"
"bytes"
"encoding/binary"
"reflect"
"strings"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEventGenerator_Events_tty(t *testing.T) {
input := "example input"
reader := bufio.NewReader(strings.NewReader(input))
g := NewEventGenerator(reader, true)
event := <-g.Events
require.NotNil(t, event, "Expected event to not be nil, but got nil")
assert.Equal(t, input, event.Message)
}
func TestEventGenerator_Events_non_tty(t *testing.T) {
input := "example input"
reader := bytes.NewReader(makeMessage(input, STDOUT))
g := NewEventGenerator(reader, false)
event := <-g.Events
require.NotNil(t, event, "Expected event to not be nil, but got nil")
assert.Equal(t, input, event.Message)
}
func TestEventGenerator_Events_non_tty_close_channel(t *testing.T) {
input := "example input"
reader := bytes.NewReader(makeMessage(input, STDOUT))
g := NewEventGenerator(reader, false)
<-g.Events
_, ok := <-g.Events
assert.False(t, ok, "Expected channel to be closed")
}
func TestEventGenerator_Events_routines_done(t *testing.T) {
input := "example input"
reader := bytes.NewReader(makeMessage(input, STDOUT))
g := NewEventGenerator(reader, false)
<-g.Events
assert.False(t, waitTimeout(&g.wg, 1*time.Second), "Expected routines to be done")
}
func makeMessage(message string, stream StdType) []byte {
data := make([]byte, 8)
binary.BigEndian.PutUint32(data[4:], uint32(len(message)))
data[0] = byte(stream / 2)
data = append(data, []byte(message)...)
return data
}
func waitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
c := make(chan struct{})
go func() {
defer close(c)
wg.Wait()
}()
select {
case <-c:
return false // completed normally
case <-time.After(timeout):
return true // timed out
}
}
func Test_createEvent(t *testing.T) {
type args struct {
message string
streamType StdType
}
tests := []struct {
name string
args args
want *LogEvent
}{
{
name: "empty message",
args: args{
message: "",
},
want: &LogEvent{
Message: "",
},
}, {
name: "simple json message",
args: args{
message: "2020-05-13T18:55:37.772853839Z {\"key\": \"value\"}",
},
want: &LogEvent{
Message: map[string]interface{}{
"key": "value",
},
},
},
{
name: "invalid json message",
args: args{
message: "2020-05-13T18:55:37.772853839Z {\"key\"}",
},
want: &LogEvent{
Message: "{\"key\"}",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := createEvent(tt.args.message, STDOUT); !reflect.DeepEqual(got.Message, tt.want.Message) {
t.Errorf("createEvent() = %v, want %v", got.Message, tt.want.Message)
}
})
}
}
type mockReadCloser struct {
bytes []byte
}
func (m mockReadCloser) Read(p []byte) (int, error) {
return copy(p, m.bytes), nil
}
func Benchmark_readEvent(b *testing.B) {
b.ReportAllocs()
data := makeMessage("2020-05-13T18:55:37.772853839Z {\"key\": \"value\"}\n", STDOUT)
reader := bufio.NewReader(mockReadCloser{bytes: data})
for i := 0; i < b.N; i++ {
readEvent(reader, true)
// println(message, stream)
}
}
+68
View File
@@ -0,0 +1,68 @@
package docker
import (
"fmt"
"log"
"net/url"
"os"
"path/filepath"
"strings"
)
type Host struct {
Name string `json:"name"`
ID string `json:"id"`
URL *url.URL `json:"-"`
CertPath string `json:"-"`
CACertPath string `json:"-"`
KeyPath string `json:"-"`
ValidCerts bool `json:"-"`
}
func ParseConnection(connection string) (Host, error) {
parts := strings.Split(connection, "|")
if len(parts) > 2 {
return Host{}, fmt.Errorf("invalid connection string: %s", connection)
}
remoteUrl, err := url.Parse(parts[0])
if err != nil {
return Host{}, err
}
name := remoteUrl.Hostname()
if len(parts) == 2 {
name = parts[1]
}
basePath, err := filepath.Abs("./certs")
if err != nil {
log.Fatalf("error converting certs path to absolute: %s", err)
}
host := remoteUrl.Hostname()
if _, err := os.Stat(filepath.Join(basePath, host)); !os.IsNotExist(err) {
basePath = filepath.Join(basePath, host)
}
cacertPath := filepath.Join(basePath, "ca.pem")
certPath := filepath.Join(basePath, "cert.pem")
keyPath := filepath.Join(basePath, "key.pem")
hasCerts := true
if _, err := os.Stat(cacertPath); os.IsNotExist(err) {
cacertPath = ""
hasCerts = false
}
return Host{
ID: strings.ReplaceAll(remoteUrl.String(), "/", ""),
Name: name,
URL: remoteUrl,
CertPath: certPath,
CACertPath: cacertPath,
KeyPath: keyPath,
ValidCerts: hasCerts,
}, nil
}
+41
View File
@@ -0,0 +1,41 @@
package docker
import (
"regexp"
"strings"
)
var KEY_VALUE_REGEX = regexp.MustCompile(`level=(\w+)`)
var ANSI_COLOR_REGEX = regexp.MustCompile(`\x1b\[[0-9;]*m`)
func guessLogLevel(logEvent *LogEvent) string {
switch value := logEvent.Message.(type) {
case string:
levels := []string{"error", "warn", "warning", "info", "debug", "trace", "fatal"}
stripped := ANSI_COLOR_REGEX.ReplaceAllString(value, "") // remove ansi color codes
for _, level := range levels {
if match, _ := regexp.MatchString("(?i)^"+level+"[^a-z]", stripped); match {
return level
}
if strings.Contains(value, "["+strings.ToUpper(level)+"]") {
return level
}
if strings.Contains(value, " "+strings.ToUpper(level)+" ") {
return level
}
}
if matches := KEY_VALUE_REGEX.FindStringSubmatch(value); matches != nil {
return matches[1]
}
case map[string]interface{}:
if level, ok := value["level"].(string); ok {
return level
}
}
return ""
}
-49
View File
@@ -1,49 +0,0 @@
package docker
import (
"bytes"
"encoding/binary"
"io"
)
type logReader struct {
readerCloser io.ReadCloser
tty bool
lastHeader []byte
buffer bytes.Buffer
}
func newLogReader(reader io.ReadCloser, tty bool) io.ReadCloser {
return &logReader{
reader,
tty,
make([]byte, 8),
bytes.Buffer{},
}
}
func (r *logReader) Read(p []byte) (n int, err error) {
if r.tty {
return r.readerCloser.Read(p)
} else {
if r.buffer.Len() > 0 {
return r.buffer.Read(p)
} else {
r.buffer.Reset()
_, err := r.readerCloser.Read(r.lastHeader)
if err != nil {
return 0, err
}
count := binary.BigEndian.Uint32(r.lastHeader[4:])
_, err = io.CopyN(&r.buffer, r.readerCloser, int64(count))
if err != nil {
return 0, err
}
return r.buffer.Read(p)
}
}
}
func (r *logReader) Close() error {
return r.readerCloser.Close()
}
+30 -3
View File
@@ -1,5 +1,9 @@
package docker
import (
"math"
)
// Container represents an internal representation of docker containers
type Container struct {
ID string `json:"id"`
@@ -11,6 +15,9 @@ type Container struct {
Created int64 `json:"created"`
State string `json:"state"`
Status string `json:"status"`
Health string `json:"health,omitempty"`
Host string `json:"host,omitempty"`
Tty bool `json:"-"`
}
// ContainerStat represent stats instant for a container
@@ -25,10 +32,30 @@ type ContainerStat struct {
type ContainerEvent struct {
ActorID string `json:"actorId"`
Name string `json:"name"`
Host string `json:"host"`
}
type LogPosition string
const (
START LogPosition = "start"
MIDDLE LogPosition = "middle"
END LogPosition = "end"
)
type LogEvent struct {
Message any `json:"m,omitempty"`
Timestamp int64 `json:"ts"`
Id uint32 `json:"id,omitempty"`
Message any `json:"m,omitempty"`
Timestamp int64 `json:"ts"`
Id uint32 `json:"id,omitempty"`
Level string `json:"l,omitempty"`
Position LogPosition `json:"p,omitempty"`
Stream string `json:"s,omitempty"`
}
func (l *LogEvent) HasLevel() bool {
return l.Level != ""
}
func (l *LogEvent) IsCloseToTime(other *LogEvent) bool {
return math.Abs(float64(l.Timestamp-other.Timestamp)) < 10
}
+10
View File
@@ -0,0 +1,10 @@
.DS_Store
.vite-ssg-dist
.vite-ssg-temp
*.local
dist
dist-ssr
node_modules
.idea/
*.log
!.vscode
+2
View File
@@ -0,0 +1,2 @@
dist
cache
+119
View File
@@ -0,0 +1,119 @@
import { createRequire } from "module";
import { defineConfig } from "vitepress";
import { createWriteStream } from "node:fs";
import { resolve } from "node:path";
import { SitemapStream, streamToPromise } from "sitemap";
const require = createRequire(import.meta.url);
const pkg = require("dozzle/package.json");
const links = [] as { url: string; lastmod?: number }[];
export default defineConfig({
lang: "en-US",
title: "Dozzle",
description: "A lightweight, open-source, and secure log viewer for Docker.",
lastUpdated: true,
cleanUrls: true,
head: [
[
"script",
{
async: "",
src: "https://www.googletagmanager.com/gtag/js?id=G-X3Z4496XFK",
},
],
[
"script",
{},
`function gtag(){dataLayer.push(arguments)}window.dataLayer=window.dataLayer||[],gtag("js",new Date),gtag("config","G-X3Z4496XFK")`,
],
],
themeConfig: {
editLink: {
pattern: "https://github.com/amir20/dozzle/edit/master/docs/:path",
},
nav: [
{ text: "Home", link: "/" },
{ text: "Guide", link: "/guide/what-is-dozzle", activeMatch: "/guide/" },
{
text: `v${pkg.version}`,
items: [
{
text: "Releases",
link: "https://github.com/amir20/dozzle/releases",
},
{
text: "New Issue",
link: "https://github.com/amir20/dozzle/issues/new/choose",
},
],
},
],
sidebar: [
{
text: "Introduction",
items: [
{ text: "What is Dozzle?", link: "/guide/what-is-dozzle" },
{ text: "Getting Started", link: "/guide/getting-started" },
],
},
{
text: "Advanced Configuration",
items: [
{ text: "Authentication", link: "/guide/authentication" },
{ text: "Healthcheck", link: "/guide/healthcheck" },
{ text: "Remote Hosts", link: "/guide/remote-hosts" },
{ text: "Supported Env Vars", link: "/guide/supported-env-vars" },
{ text: "Logging Files on Disk", link: "/guide/log-files-on-disk" },
{ text: "Analytics", link: "/guide/analytics" },
],
},
{
text: "Troubleshooting",
items: [
{ text: "FAQ", link: "/guide/faq" },
{ text: "Debugging", link: "/guide/debugging" },
],
},
],
footer: {
message: "Released under the MIT License. Open sourced and sponsored by Docker OSS.",
copyright: "Copyright © 2019-present <a href='https://amirraminfar.me'>Amir Raminfar</a>",
},
socialLinks: [
{ icon: "github", link: "https://github.com/amir20/dozzle" },
{
icon: {
svg: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M21.81 10.25c-.06-.04-.56-.43-1.64-.43c-.28 0-.56.03-.84.08c-.21-1.4-1.38-2.11-1.43-2.14l-.29-.17l-.18.27c-.24.36-.43.77-.51 1.19c-.2.8-.08 1.56.33 2.21c-.49.28-1.29.35-1.46.35H2.62c-.34 0-.62.28-.62.63c0 1.15.18 2.3.58 3.38c.45 1.19 1.13 2.07 2 2.61c.98.6 2.59.94 4.42.94c.79 0 1.61-.07 2.42-.22c1.12-.2 2.2-.59 3.19-1.16A8.3 8.3 0 0 0 16.78 16c1.05-1.17 1.67-2.5 2.12-3.65h.19c1.14 0 1.85-.46 2.24-.85c.26-.24.45-.53.59-.87l.08-.24l-.19-.14m-17.96.99h1.76c.08 0 .16-.07.16-.16V9.5c0-.08-.07-.16-.16-.16H3.85c-.09 0-.16.07-.16.16v1.58c.01.09.07.16.16.16m2.43 0h1.76c.08 0 .16-.07.16-.16V9.5c0-.08-.07-.16-.16-.16H6.28c-.09 0-.16.07-.16.16v1.58c.01.09.07.16.16.16m2.47 0h1.75c.1 0 .17-.07.17-.16V9.5c0-.08-.06-.16-.17-.16H8.75c-.08 0-.15.07-.15.16v1.58c0 .09.06.16.15.16m2.44 0h1.77c.08 0 .15-.07.15-.16V9.5c0-.08-.06-.16-.15-.16h-1.77c-.08 0-.15.07-.15.16v1.58c0 .09.07.16.15.16M6.28 9h1.76c.08 0 .16-.09.16-.18V7.25c0-.09-.07-.16-.16-.16H6.28c-.09 0-.16.06-.16.16v1.57c.01.09.07.18.16.18m2.47 0h1.75c.1 0 .17-.09.17-.18V7.25c0-.09-.06-.16-.17-.16H8.75c-.08 0-.15.06-.15.16v1.57c0 .09.06.18.15.18m2.44 0h1.77c.08 0 .15-.09.15-.18V7.25c0-.09-.07-.16-.15-.16h-1.77c-.08 0-.15.06-.15.16v1.57c0 .09.07.18.15.18m0-2.28h1.77c.08 0 .15-.07.15-.16V5c0-.1-.07-.17-.15-.17h-1.77c-.08 0-.15.06-.15.17v1.56c0 .08.07.16.15.16m2.46 4.52h1.76c.09 0 .16-.07.16-.16V9.5c0-.08-.07-.16-.16-.16h-1.76c-.08 0-.15.07-.15.16v1.58c0 .09.07.16.15.16"/></svg>`,
},
link: "https://hub.docker.com/r/amir20/dozzle",
},
],
},
transformHtml: (_, id, { pageData }) => {
if (!/[\\/]404\.html$/.test(id))
links.push({
// you might need to change this if not using clean urls mode
url: pageData.relativePath.replace(/((^|\/)index)?\.md$/, "$2"),
lastmod: pageData.lastUpdated,
});
},
buildEnd: async ({ outDir }) => {
const sitemap = new SitemapStream({
hostname: "https://dozzle.dev/",
});
const writeStream = createWriteStream(resolve(outDir, "sitemap.xml"));
sitemap.pipe(writeStream);
links.forEach((link) => sitemap.write(link));
const promise = streamToPromise(sitemap);
sitemap.end();
await promise;
},
});
@@ -0,0 +1,14 @@
<script setup></script>
<template>
<div border="~ solid rounded light-100 dark:dark-50" bg="[#eee] dark:[#222]">
<div flex gap="2" p-2>
<div circle-red />
<div circle-yellow />
<div circle-green />
</div>
<div class="content">
<slot />
</div>
</div>
</template>
@@ -0,0 +1,44 @@
<script lang="ts" setup>
import { useMutationObserver } from "@vueuse/core";
import { onMounted, onUnmounted, ref } from "vue";
const isDark = ref(false);
onMounted(() => {
isDark.value = document.documentElement.classList.contains("dark");
useMutationObserver(
document.documentElement,
(mutations) => {
isDark.value = document.documentElement.classList.contains("dark");
},
{
attributes: true,
attributeFilter: ["class"],
},
);
});
onMounted(() => {
document.documentElement.classList.add("home");
});
onUnmounted(() => {
document.documentElement.classList.remove("home");
});
</script>
<template>
<browser-window drop-shadow-md>
<video muted loop autoplay playsinline poster="../media/poster.png" v-if="isDark">
<source src="../media/dozzle-dark.webm" type="video/webm" />
<source src="../media/dozzle-dark.mp4" type="video/mp4" />
<img src="../media/poster.png" alt="" />
</video>
<video muted loop autoplay playsinline poster="../media/poster.png" v-else>
<source src="../media/dozzle-light.webm" type="video/webm" />
<source src="../media/dozzle-light.mp4" type="video/mp4" />
<img src="../media/poster.png" alt="" />
</video>
</browser-window>
</template>
<style scoped></style>
+16
View File
@@ -0,0 +1,16 @@
// https://vitepress.dev/guide/custom-theme
import { h } from "vue";
import Theme from "vitepress/theme";
import "uno.css";
import "./style.css";
import HeroVideo from "./components/HeroVideo.vue";
export default {
...Theme,
Layout: () => {
return h(Theme.Layout, null, {
"home-hero-image": () => h(HeroVideo),
});
},
enhanceApp({ app, router, siteData }) {},
};
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 KiB

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