Compare commits

...

259 Commits

Author SHA1 Message Date
Amir Raminfar b066433933 chore: release v5.4.2 2023-11-14 13:06:04 -08:00
Amir Raminfar 52dc1ff2d1 fix: fixes collapse button to not cover the menu on left. see #2500 (#2501) 2023-11-14 13:05:01 -08:00
Suresh Ramakrishnaiah 10ea7cfb51 docs: update authentication.md (#2492) 2023-11-13 18:41:25 -08:00
renovate[bot] eea9813f84 fix(deps): update all non-major dependencies (#2498)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-14 00:52:30 +00:00
Amir Raminfar 8ac79c707c chore: updates go version for CI (#2499) 2023-11-14 00:48:28 +00:00
Amir Raminfar 4637b49075 chore: fixes golang version 2023-11-13 14:29:21 -08:00
renovate[bot] 147902f191 fix(deps): update all non-major dependencies (#2493)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-13 14:28:59 -08:00
Amir Raminfar f83b5c948e fix: converts HSL colors OKLCH with DaisyUI v4 (#2497) 2023-11-13 14:00:35 -08:00
Amir Raminfar 542c895812 chore: fixes padding 2023-11-11 14:08:11 -08:00
Amir Raminfar d14d140cb6 chore(test): adds test for simple mode (#2491) 2023-11-11 22:04:45 +00:00
Amir Raminfar be74d48de7 fix: improves the alignment of drop downs 2023-11-11 13:48:14 -08:00
Amir Raminfar f8a9396f6e chore(test): adds test for proxy mode (#2490) 2023-11-11 13:22:06 -08:00
Amir Raminfar 009529f20e chore: release v5.4.1 2023-11-11 06:41:45 -08:00
Amir Raminfar 63f16ef575 fix: fixes proxy pass not finding user (#2488) 2023-11-11 06:37:06 -08:00
Amir Raminfar 9de964dac4 chore: release v5.4.0 2023-11-10 15:20:41 -08:00
Amir Raminfar 1ff0b5541d chore: fixes incorrect version in doc 2023-11-10 13:47:30 -08:00
Amir Raminfar 55326cdd41 docs: adds sponsor section (#2487) 2023-11-10 21:08:32 +00:00
Amir Raminfar 9fa4911aec feat: list of releases can now be seen at the top (#2480) 2023-11-10 19:45:47 +00:00
Amir Raminfar 7dae54df26 chore: fixes golang version 2023-11-10 09:04:30 -08:00
renovate[bot] 1b101e62da chore(deps): update all non-major dependencies (#2485)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-10 09:04:07 -08:00
Amir Raminfar 5c5cd17fb6 chore: removes netlify files 2023-11-10 08:46:27 -08:00
Amir Raminfar c697be659b docs: adds docs for custom content (#2483) 2023-11-08 14:58:48 -08:00
Amir Raminfar d469066c07 feat: moves user's name and email to dropdown (#2479) 2023-11-07 11:07:34 -08:00
Amir Raminfar 4380681812 feat: supports default avatar when gravatar is missing (#2478) 2023-11-07 09:26:48 -08:00
Amir Raminfar 75cf2f045a chore: release v5.3.1 2023-11-06 15:29:56 -08:00
Amir Raminfar ab5807b3ae chore: fixes go.mod 2023-11-06 15:24:50 -08:00
renovate[bot] fe0f163090 chore(deps): update all non-major dependencies (#2477)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-06 15:23:04 -08:00
Amir Raminfar 38d1f99387 fix: reduces padding on mobile (#2476) 2023-11-06 21:59:53 +00:00
Amir Raminfar 11298e83b0 chore: release v5.3.0 2023-11-06 12:39:29 -08:00
Amir Raminfar 32de313c27 feat: supports having custom content with markdown (#2466) 2023-11-06 12:39:11 -08:00
renovate[bot] 1e1c64d6e7 fix(deps): update all non-major dependencies (#2474)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-06 01:13:20 +00:00
Amir Raminfar 0f02de2070 chore: release v5.2.3 2023-11-05 12:30:54 -08:00
Amir Raminfar bb085affb2 fix: removes fetch request when user does not exist (#2473) 2023-11-05 12:29:44 -08:00
Amir Raminfar 1d022b51b5 fix: fixes bad icon (#2472) 2023-11-04 20:53:24 +00:00
Amir Raminfar eb31b46e6f fix: fixes a potential rare edge case that log entry has no payload. see #2451. (#2471) 2023-11-04 13:51:29 -07:00
Amir Raminfar 066541288a docs: updates readme (#2470) 2023-11-03 21:10:21 +00:00
renovate[bot] eba7d92ba6 fix(deps): update all non-major dependencies (#2468)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-03 17:44:40 +00:00
Jacek Dobosz 579ae12920 docs: installation on podman (#2467) 2023-11-03 05:41:48 -07:00
Amir Raminfar ba9531dc59 chore: release v5.2.2 2023-11-01 12:36:12 -07:00
Amir Raminfar c4cc233720 chore: updates go.mod 2023-11-01 12:13:47 -07:00
Amir Raminfar 6c1f59c7e3 feat: removes request analytics on page load (#2464) 2023-11-01 12:13:17 -07:00
Amir Raminfar ebc02900b6 feat: prints local docker engine error 2023-10-31 16:02:58 -07:00
Amir Raminfar 4e5f7ae1cf chore: release v5.2.1 2023-10-31 15:11:58 -07:00
Amir Raminfar 55f076382e fix: fixes not being able to create /data dir (#2463) 2023-10-31 15:11:15 -07:00
Amir Raminfar cae8f2740d feat: warns against using username and password 2023-10-31 12:45:30 -07:00
Amir Raminfar c75b5a598d chore: fixes typo 2023-10-31 12:33:43 -07:00
Amir Raminfar 701f16fcc2 chore: release v5.2.0 2023-10-31 12:33:12 -07:00
Amir Raminfar 49d53ce255 chore(docs): updates gh pages to have simple auth 2023-10-31 12:30:39 -07:00
Amir Raminfar e57d038221 fix: changes mime type for fetching between to application/x-jsonl. see #2458 (#2461) 2023-10-31 11:22:27 -07:00
Amir Raminfar c74384d012 fix: fixes blank name for proxy mode 2023-10-31 11:15:50 -07:00
Amir Raminfar 5a068133bf fix: fixes forward proxy bug not working (#2460) 2023-10-30 22:42:40 +00:00
Amir Raminfar e85a4aefa2 feat: adds support for user.yml from disk (#2455) 2023-10-30 12:20:00 -07:00
Amir Raminfar b0e43333d2 chore: release v5.1.2 2023-10-30 08:20:25 -07:00
renovate[bot] d2c71e2dc9 fix(deps): update dependency vue-i18n to ^9.6.2 (#2459)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-30 03:56:24 +00:00
renovate[bot] 7fdaa3e4f0 chore(deps): update actions/setup-node action to v4 (#2457)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-29 18:50:14 -07:00
renovate[bot] 0344e2adb4 chore(deps): update actions/checkout action to v4 (#2456)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-29 18:49:54 -07:00
Amir Raminfar 7431c3455f chore(docs): enables gh pages 2023-10-27 13:00:51 -07:00
Amir Raminfar 1fe46e605a feat: syncs settings to disk for authenticated users (#2445) 2023-10-27 12:53:42 -07:00
renovate[bot] e4d1a18d37 fix(deps): update all non-major dependencies (#2453)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-27 19:44:15 +00:00
Amir Raminfar a0a2b7175c chore: release v5.1.1 2023-10-27 11:22:01 -07:00
Amir Raminfar b88fc3c5d1 feat: makes hour time format always 2 digits (#2452) 2023-10-27 09:46:04 -07:00
Amir Raminfar a8bc1dd4c2 chore: reverts netlify changes 2023-10-26 13:45:09 -07:00
Amir Raminfar 29fcb53fac chore: uses removes store 2023-10-26 13:34:38 -07:00
Amir Raminfar 4dfd57612d chore: rollback pnpm changes 2023-10-26 11:28:43 -07:00
Amir Raminfar 51b9ad5f00 chore: tries to clear pnpm store prune again 2023-10-26 10:29:21 -07:00
Amir Raminfar f4b53fd09d chore: tries to clear pnpm store prune 2023-10-26 10:28:49 -07:00
renovate[bot] 5d5628530b fix(deps): update dependency fuse.js to v7 (#2447)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-26 09:47:36 -07:00
Amir Raminfar 4d0904bb2c chore: cleans pnpm 2023-10-26 09:41:27 -07:00
renovate[bot] 13ebc06812 fix(deps): update all non-major dependencies (#2446)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-25 22:09:14 +00:00
Amir Raminfar 7fbf760e9a chore: release v5.1.0 2023-10-25 14:01:40 -07:00
Amir Raminfar 9a2935d3d4 chore: fixes typos 2023-10-25 13:56:56 -07:00
Amir Raminfar fd24659903 chore(docs): adds docks for forward-proxy 2023-10-25 13:48:11 -07:00
renovate[bot] d2a43b7e6f chore(deps): update actions/setup-node action to v4 (#2444) 2023-10-24 06:43:12 -07:00
Amir Raminfar 2a07ca39db chore(refactor): refactors go code (#2443) 2023-10-24 06:28:16 -07:00
Amir Raminfar 74e44bd5f2 fix: fixes log levels that are not in the start of the line 2023-10-23 19:06:04 -07:00
Amir Raminfar be11c32139 feat: adds forward-proxy (#2442) 2023-10-23 15:31:39 -07:00
Amir Raminfar e55df82023 chore: release v5.0.10 2023-10-23 09:01:18 -07:00
Amir Raminfar 78c1d72f3d feat: adds more levels parsing like [ error] with spaces (#2441) 2023-10-23 09:00:29 -07:00
renovate[bot] caa3bb14ab chore(deps): update dependency vitepress to v1.0.0-rc.23 (#2440)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-23 00:58:43 +00:00
renovate[bot] 2881f25a1a chore(deps): update all non-major dependencies (#2435)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-21 16:39:44 +00:00
Amir Raminfar 11a972b048 fix: fixes broken icons (#2438) 2023-10-21 09:36:11 -07:00
Amir Raminfar 8fc308d294 chore: updates vue 2023-10-20 12:32:32 -07:00
renovate[bot] 33e7b84061 chore(deps): update node.js to v21 (#2436)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-20 12:28:45 -07:00
Amir Raminfar 470d6ed0d9 chore: release v5.0.9 2023-10-20 11:57:56 -07:00
Amir Raminfar 01f3c5ee15 fix: fixes reading incorrect number of bytes. see #2431 (#2434) 2023-10-20 11:51:06 -07:00
Amir Raminfar 79f8efd909 feat: supports lower case level like [info] (#2433) 2023-10-20 18:12:22 +00:00
Amir Raminfar 021d3f7a99 chore: updates node modules 2023-10-18 14:12:28 -07:00
Amir Raminfar bf79b213c6 chore(docs): update security readme. (#2430) 2023-10-18 12:31:03 -07:00
Amir Raminfar f216587c9b chore: release v5.0.8 2023-10-16 11:46:10 -07:00
Amir Raminfar 803b66188e chore: code clean up (#2428) 2023-10-16 11:45:43 -07:00
Amir Raminfar 37d2b07720 fix: reverts broken changes to event source (#2427) 2023-10-16 11:37:23 -07:00
Amir Raminfar 9879004ae3 feat: adds workflow for semantic messages 2023-10-16 10:11:14 -07:00
Amir Raminfar 0c5c08deeb chore: release v5.0.7 2023-10-16 10:01:06 -07:00
Peter Dave Hello 89a6fbfab2 Add zh-tw locale (#2425) 2023-10-16 09:59:29 -07:00
Amir Raminfar 5b1f3892a7 chore: release v5.0.6 2023-10-16 09:26:50 -07:00
Amir Raminfar 37c6033371 fix: cleans up reading messages when headers are invalid (#2424) 2023-10-16 09:26:27 -07:00
Amir Raminfar 4f748bf9e1 fix: fixes swallowed error when bad headers are found and fixing #2416 (#2423)
* fix: fixes swallowed error when bad headers are found and fixing #2416

* minor clean up

* fixes broken on first connect

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

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

* feat: removes error from event streams

* removes unused code

* adds errors back

* moves logs to debug

* removes lastEventId

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

* removes unused code

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

* complete the flow with alerts and settings page

* adds more langs and option for once

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

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

* fixes tests

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

* update styles

* creates toggle component

* adds drop down component

* cleans up components

* removes unused components

* uses tailwind for scroll view

* removes table component

* improves animation

* cleans up more styles

* uses more tailwind

* cleans up more styles with flex

* more styles

* removes bulma

* adds colors

* updates modules

* fixes bugs

* stops importing styles.scss

* more clean up

* cleans up headers

* cleans up title

* fixes title

* fixes mobile-hidden

* fixes shadow

* fixes colors

* add tailwindcss/nesting

* adds more colors

* fixes more colors

* updates colors

* fixes colors

* colors

* fixes menu on left

* menu and modal

* menu and modal

* fuzzy search

* fixes menu on left

* remove logs

* cleans up search

* adds host to search

* remove outline from inputs

* cleans up left search icon

* removes unused styles

* fixes docker

* removes sass!

* cleans up styles

* Fixe smobile menu

* fixes mobile menu

* fixes typecheck

* fixes seconday color

* adds drop down for container

* cleans header css

* updates css

* fixes other layouts

* updates some tests

* fixes border

* fixes home screen font

* fixes top header

* fixes tests

* fixes fieldlist

* fixes complex

* cleans up more

* removes index

* fixes tests

* fixes tests

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

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

* fixes tests
2023-08-25 13:42:00 -07:00
Amir Raminfar 512b181b48 fix: adds ellipses for titles that have swarm id (#2368) 2023-08-25 13:33:03 -07:00
Amir Raminfar be5e496380 chore: updates node modules 2023-08-25 11:00:27 -07:00
Amir Raminfar 887670ebfe chore: release v4.11.3 2023-08-25 10:47:56 -07:00
Amir Raminfar 9cdc8ad8f5 feat: adds host to container table on homepage (#2367)
* feat: adds host to container table on homepage

* more translations
2023-08-25 17:26:39 +00:00
renovate[bot] 0dd68b22ca fix(deps): update all non-major dependencies 2023-08-25 17:18:21 +00:00
Amir Raminfar 60ac81ff94 chore: code clean up on index 2023-08-25 10:14:37 -07:00
Amir Raminfar a79495cdfc chore: code clean up 2023-08-25 10:06:47 -07:00
Amir Raminfar 409363b26b chore: cleans up reflex 2023-08-25 10:00:41 -07:00
codeman1o1 2aaaf2ba1f Improve page title handling (#2365)
I tried my best with the translations.
Google Translate gives me the right English translation back for each language
2023-08-25 09:58:02 -07:00
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
180 changed files with 6934 additions and 5099 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base", ":automergeMinor"]
"extends": ["config:base", "schedule:weekly", "group:allNonMajor", ":automergeMinor"],
"rangeStrategy": "bump"
}
+1
View File
@@ -6,6 +6,7 @@ daysUntilClose: 3
exemptLabels:
- pinned
- security
- keep
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
+18 -17
View File
@@ -8,14 +8,14 @@ jobs:
name: JavaScript Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
name: Install Node
with:
node-version: latest
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: latest
cache: "pnpm"
@@ -31,9 +31,10 @@ jobs:
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: 1.20.x
go-version: 1.21.x
check-latest: true
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Run Go Tests with Coverage
run: make test SKIP_ASSET=1
int-test:
@@ -41,14 +42,14 @@ jobs:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
name: Install Node
with:
node-version: latest
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: latest
cache: "pnpm"
@@ -56,9 +57,9 @@ jobs:
- name: Install dependencies
run: pnpm install
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Build
uses: docker/bake-action@v3
uses: docker/bake-action@v4
with:
load: true
set: |
@@ -79,21 +80,21 @@ jobs:
steps:
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
images: amir20/dozzle
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.9.1
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
- name: Login to DockerHub
uses: docker/login-action@v2.2.0
uses: docker/login-action@v3.0.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v4.1.1
uses: docker/build-push-action@v5.0.0
with:
push: true
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }}
build-args: TAG=${{ steps.meta.outputs.version }}
labels: ${{ steps.meta.outputs.labels }}
@@ -105,11 +106,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
- name: Release to Github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+5 -5
View File
@@ -14,21 +14,21 @@ jobs:
steps:
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
images: amir20/dozzle
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.9.1
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
- name: Login to DockerHub
uses: docker/login-action@v2.2.0
uses: docker/login-action@v3.0.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v4.1.1
uses: docker/build-push-action@v5.0.0
with:
push: true
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }}
build-args: TAG=${{ steps.meta.outputs.version }}
labels: ${{ steps.meta.outputs.labels }}
+62
View File
@@ -0,0 +1,62 @@
# Sample workflow for building and deploying a VitePress site to GitHub Pages
#
name: Deploy VitePress site to Pages
on:
push:
branches: [master]
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: pages
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # Not needed if lastUpdated is not enabled
- uses: pnpm/action-setup@v2
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: latest
cache: pnpm # or pnpm / yarn
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Install dependencies
run: pnpm install
- name: Build with VitePress
run: |
pnpm docs:build
touch docs/.vitepress/dist/.nojekyll
- name: Upload artifact
uses: actions/upload-pages-artifact@v2
with:
path: docs/.vitepress/dist
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
name: Deploy
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
+20
View File
@@ -0,0 +1,20 @@
name: "Lint PR"
on:
pull_request_target:
types:
- opened
- edited
- synchronize
permissions:
pull-requests: read
jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+14 -13
View File
@@ -11,14 +11,14 @@ jobs:
name: Typecheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
name: Install Node
with:
node-version: latest
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: latest
cache: "pnpm"
@@ -31,14 +31,14 @@ jobs:
name: JavaScript Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
name: Install Node
with:
node-version: latest
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: latest
cache: "pnpm"
@@ -54,9 +54,10 @@ jobs:
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: 1.20.x
go-version: 1.21.x
check-latest: true
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Run Go Tests with Coverage
run: make test SKIP_ASSET=1
int-test:
@@ -64,14 +65,14 @@ jobs:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
name: Install Node
with:
node-version: latest
- run: corepack enable
- run: pnpm --version
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: latest
cache: "pnpm"
@@ -79,9 +80,9 @@ jobs:
- name: Install dependencies
run: pnpm install
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Build
uses: docker/bake-action@v3
uses: docker/bake-action@v4
with:
load: true
set: |
+1 -1
View File
@@ -1,4 +1,5 @@
certs
data
dist
node_modules
.cache
@@ -6,7 +7,6 @@ static
dozzle
coverage
.pnpm-debug.log
.vscode
coverage.out
.netlify
/test-results/
+1
View File
@@ -1,3 +1,4 @@
module.exports = {
printWidth: 120,
plugins: ["prettier-plugin-tailwindcss"],
};
+1 -1
View File
@@ -1 +1 @@
-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
-r '\.(go)$' -R 'node_modules' -G '\*\_test.go' -s -- go run main.go --level debug
+6
View File
@@ -0,0 +1,6 @@
{
"i18n-ally.localesPaths": ["locales"],
"i18n-ally.keystyle": "nested",
"cSpell.words": ["healthcheck"],
"editor.formatOnSave": true
}
+6 -8
View File
@@ -1,5 +1,5 @@
# Build assets
FROM --platform=$BUILDPLATFORM node:20-alpine as node
FROM --platform=$BUILDPLATFORM node:21-alpine as node
RUN corepack enable
@@ -14,7 +14,7 @@ COPY package.json ./
RUN pnpm install --offline --ignore-scripts --no-optional
# Copy assets and translations to build
COPY .* vite.config.ts ./
COPY .* *.config.ts *.config.js ./
COPY assets ./assets
COPY locales ./locales
COPY public ./public
@@ -22,7 +22,7 @@ COPY public ./public
# Build assets
RUN pnpm build
FROM --platform=$BUILDPLATFORM golang:1.20.6-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.21.4-alpine AS builder
RUN apk add --no-cache ca-certificates && mkdir /dozzle
@@ -36,10 +36,7 @@ RUN go mod download
COPY --from=node /build/dist ./dist
# Copy all other files
COPY analytics ./analytics
COPY healthcheck ./healthcheck
COPY docker ./docker
COPY web ./web
COPY internal ./internal
COPY main.go ./
# Args
@@ -49,11 +46,12 @@ ARG TARGETOS TARGETARCH
# Build binary
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=$TAG" -o dozzle
RUN mkdir /data
FROM scratch
ENV PATH /bin
COPY --from=builder /data /data
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=builder /dozzle/dozzle /dozzle
+40 -145
View File
@@ -15,71 +15,27 @@ https://user-images.githubusercontent.com/260667/227634771-9ebbe381-16a8-465a-b2
- Search logs using regex 🔦
- Small memory footprint 🏎
- Split screen for viewing multiple logs
- Download logs easy
- Download logs easily
- Live stats with memory and CPU usage
- Authentication with username and password 🚨
- Multi-user authentication with support for proxy forward authorization 🚨
Dozzle should work for most. It has been tested with hundreds of containers. However, it doesn't support offline searching. Products like [Loggly](https://www.loggly.com), [Papertrail](https://papertrailapp.com) or [Kibana](https://www.elastic.co/products/kibana) are more suited for full search capabilities.
Dozzle has been tested with hundreds of containers. However, it doesn't support offline searching. Products like [Loggly](https://www.loggly.com), [Papertrail](https://papertrailapp.com) or [Kibana](https://www.elastic.co/products/kibana) are more suited for full search capabilities.
Dozzle doesn't cost any money and aims to focus on real-time debugging.
## Getting Started
## Getting Dozzle
Dozzle is a very small Docker container (4 MB compressed). Pull the latest release from the index:
Dozzle is a small container (4 MB compressed). Pull the latest release with:
$ docker pull amir20/dozzle:latest
## Using Dozzle
### Running Dozzle
The simplest way to use dozzle is to run the docker container. Also, mount the Docker Unix socket with `--volume` to `/var/run/docker.sock`:
$ docker run --name dozzle -d --volume=/var/run/docker.sock:/var/run/docker.sock -p 8888:8080 amir20/dozzle:latest
Dozzle will be available at [http://localhost:8888/](http://localhost:8888/). You can change `-p 8888:8080` to any port. For example, if you want to view dozzle over port 4040 then you would do `-p 4040:8080`.
Dozzle will be available at [http://localhost:8888/](http://localhost:8888/).
### Connecting with Docker compose
version: "3"
services:
dozzle:
container_name: dozzle
image: amir20/dozzle:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
ports:
- 9999:8080
### 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.
Here is the Docker Compose file:
version: "3"
services:
@@ -90,49 +46,50 @@ Dozzle doesn't enable healthcheck by default as it adds extra CPU usage. `health
- /var/run/docker.sock:/var/run/docker.sock
ports:
- 8080:8080
environment:
DOZZLE_LEVEL: trace
healthcheck:
test: [ "CMD", "/dozzle", "healthcheck" ]
interval: 3s
timeout: 30s
retries: 5
start_period: 30s
#### Security
For advanced options like [authentication](https://dozzle.dev/guide/authentication), [remote hosts](https://dozzle.dev/guide/remote-hosts) or common [questions](https://dozzle.dev/guide/faq) see documentation at [dozzle.dev](https://dozzle.dev/guide/getting-started).
You can control the device Dozzle binds to by passing `--addr` parameter. For example,
## Technical Details
$ docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8888:1224 amir20/dozzle:latest --addr localhost:1224
Dozzle users automatic API negotiation which works with most Docker configurations. Dozzle also works with [Colima](https://github.com/abiosoft/colima) and [Podman](https://podman.io/).
will bind to `localhost` on port `1224`. You can then use a reverse proxy to control who can see dozzle.
### Installation on podman
If you wish to restrict the containers shown you can pass the `--filter` parameter. For example,
By default Podman doesn't have a background process but you can enable this for Dozzle to work.
$ docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8888:1224 amir20/dozzle:latest --filter name=foo
Verify first if your podman installation has enabled remote socket:
this would then only allow you to view containers with a name starting with "foo". You can use other filters like `status` as well, please check the official docker [command line docs](https://docs.docker.com/engine/reference/commandline/ps/#filtering) for available filters. Multiple `--filter` arguments can be provided.
```
podman info
```
#### Authentication
When you get under the key remote socket output like this, its already enabled:
Dozzle supports a very simple authentication out of the box with just username and password. You should deploy using SSL to keep the credentials safe. See configuration to use `--username` and `--password`. You can also use [docker secrets](https://docs.docker.com/engine/swarm/secrets/) `--usernamefile` and `--passwordfile`.
```
remoteSocket:
exists: true
path: /run/user/1000/podman/podman.sock
```
#### Changing base URL
If it's not enabled please follow [this tutorial](https://github.com/containers/podman/blob/main/docs/tutorials/socket_activation.md) to enable it.
Dozzle by default mounts to "/". If you want to control the base path you can use the `--base` option. For example, if you want to mount at "/foobar",
then you can override by using `--base /foobar`. See env variables below for using `DOZZLE_BASE` to change this.
Once you have the podman remote socket you can run Dozzle on podman.
$ docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir20/dozzle:latest --base /foobar
```
podman run --volume=/run/user/1000/podman/podman.sock:/var/run/docker.sock -d -p 8080:8080 amir20/dozzle:latest
```
Dozzle will be available at [http://localhost:8080/foobar/](http://localhost:8080/foobar/).
## Security
#### Analytics collected
Dozzle supports file based authentication and forward proxy like [Authelia](https://www.authelia.com/). These are documented at https://dozzle.dev/guide/authentication.
## Analytics collected
Dozzle collects anonymous user configurations using Google Analytics. Why? Dozzle is an open source project with no funding. As a result, there is no time to do user studies of Dozzle. Analytics is collected to prioritize features and fixes based on how people use Dozzle. This data is completely public and can be viewed live using [ Data Studio dashboard](https://datastudio.google.com/s/naeIu0MiWsY).
If you do not want to be tracked at all, see the `--no-analytics` flag below.
#### Environment variables and configuration
## Environment variables and configuration
Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can use the CLI flags or environment variables. The table below outlines all supported options and their respective env vars.
@@ -150,77 +107,15 @@ Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
| `--remote-host` | `DOZZLE_REMOTE_HOST` | |
## Troubleshooting and FAQs
## Support
<details>
<summary>I installed Dozzle, but logs are slow or they never load. Help!</summary>
There are many ways you can support Dozzle:
Dozzle uses Server Sent Events (SSE) which connects to a server using a HTTP stream without closing the connection. If any proxy tries to buffer this connection, then Dozzle never receives the data and hangs forever waiting for the reverse proxy to flush the buffer. Since version `1.23.0`, Dozzle sends the `X-Accel-Buffering: no` header which should stop reverse proxies buffering. However, some proxies may ignore this header. In those cases, you need to explicitly disable any buffering.
- Use it! Write about it! Star it! If you love Dozzle, drop me a line and tell me what you love.
- Blog about Dozzle to spread the word. If you are good at writing send PRs to improve the documentation at [dozzle.dev](https://dozzle.dev/)
- Sponsor my work at https://www.buymeacoffee.com/amirraminfar
Below is an example with nginx and using `proxy_pass` to disable buffering.
```
server {
...
location / {
proxy_pass http://<dozzle.container.ip.address>:8080;
}
location /api {
proxy_pass http://<dozzle.container.ip.address>:8080;
proxy_buffering off;
proxy_cache off;
}
}
```
</details>
<details>
<summary>What data does Dozzle collect?</summary>
Dozzle does collect some analytics. Analytics is anonymous usage tracking of the features which are used the most. See the section above on how to disable any analytic collection.
In the browser, Dozzle has a [strict](https://github.com/amir20/dozzle/blob/master/web/csp.go#L9) Content Security Policy which only allows the following policies:
- Allow connect to `api.github.com` to fetch most recent version.
- Only allow `<script>` and `<style>` files from `self`
Dozzle opens all links with `rel="noopener"`.
</details>
<details>
<summary>We have tools that uses Dozzle when a new container is created. How can I get a direct link to a container by name?</summary>
Dozzle has a [special route](https://github.com/amir20/dozzle/blob/master/assets/pages/Show.vue) that can be used to search containers by name and then forward to that container. For example, if you have a container with name `"foo.bar"` and id `abc123`, you can send your users to `/show?name=foo.bar` which will be forwarded to `/container/abc123`.
</details>
<details>
<summary>I installed Dozzle but memory consumption doesn't show up!</summary>
_This is an issue specific to ARM devices_
Dozzle uses the Docker API to gather information about the containers' memory usage. If the memory usage is not showing up, then it is likely that the Docker API is not returning the memory usage.
You can verify this by running `docker info`, and you should see the following:
```
WARNING: No memory limit support
WARNING: No swap limit support
```
In this case, you'll need to add the following line to your `/boot/cmdline.txt` file and reboot your device.
```
cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1
```
</details>
<a href="https://www.buymeacoffee.com/amirraminfar" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
## License
@@ -232,6 +127,6 @@ To Build and test locally:
1. Install [NodeJs](https://nodejs.org/en/download/) and [pnpm](https://pnpm.io/installation).
2. Install [Go](https://go.dev/doc/install).
3. Install [reflex](https://github.com/cespare/reflex) with `go get -u github.com/cespare/reflex` outside of dozzle.
3. Install [reflex](https://github.com/cespare/reflex) with `go install github.com/cespare/reflex@latest`.
4. Install node modules `pnpm install`.
5. Do `pnpm dev`
+5
View File
@@ -0,0 +1,5 @@
# Security Policy
## Reporting a Vulnerability
Dozzle is checked daily with https://docs.docker.com/scout/ and its dependencies are updated regularly. If you find a security bug, please update to the latest. For vulnerability on latest images please file an issue on Github.
+30 -1
View File
@@ -22,5 +22,34 @@ watchEffect(() => {
}
});
</script>
<style lang="postcss">
html.has-custom-scrollbars {
::-webkit-scrollbar {
width: 8px;
display: content;
}
<style scoped lang="scss"></style>
::-webkit-scrollbar-thumb {
background-color: rgba(128, 128, 128, 0.33);
outline: 1px solid slategrey;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:active {
background-color: #777;
}
::-webkit-scrollbar-track {
background-color: transparent;
}
::-webkit-scrollbar-track:hover {
background-color: rgba(64, 64, 64, 0.33);
}
section main {
scrollbar-color: #353535 transparent;
scrollbar-width: thin;
}
}
</style>
+119 -64
View File
@@ -12,26 +12,30 @@ declare global {
const $ref: typeof import('vue/macros')['$ref']
const $shallowRef: typeof import('vue/macros')['$shallowRef']
const $toRef: typeof import('vue/macros')['$toRef']
const DEFAULT_SETTINGS: typeof import('./composables/settings')['DEFAULT_SETTINGS']
const DEFAULT_SETTINGS: typeof import('./stores/settings')['DEFAULT_SETTINGS']
const EffectScope: typeof import('vue')['EffectScope']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const arrayEquals: typeof import('./utils/index')['arrayEquals']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const collapseNav: typeof import('./composables/settings')['collapseNav']
const automaticRedirect: typeof import('./stores/settings')['automaticRedirect']
const collapseNav: typeof import('./stores/settings')['collapseNav']
const computed: typeof import('vue')['computed']
const computedAsync: typeof import('@vueuse/core')['computedAsync']
const computedEager: typeof import('@vueuse/core')['computedEager']
const computedInject: typeof import('@vueuse/core')['computedInject']
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
const config: typeof import('./stores/config')['default']
const containerContext: typeof import('./composable/containerContext')['containerContext']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: typeof import('vue')['createApp']
const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGenericProjection: typeof import('@vueuse/math')['createGenericProjection']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createPinia: typeof import('pinia')['createPinia']
const createProjection: typeof import('@vueuse/math')['createProjection']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
@@ -52,19 +56,23 @@ declare global {
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const getDeep: typeof import('./utils/index')['getDeep']
const globalShowPopup: typeof import('./composables/popup')['globalShowPopup']
const globalShowPopup: typeof import('./composable/popup')['globalShowPopup']
const h: typeof import('vue')['h']
const hourStyle: typeof import('./composables/settings')['hourStyle']
const hourStyle: typeof import('./stores/settings')['hourStyle']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const injectLocal: typeof import('@vueuse/core')['injectLocal']
const isDefined: typeof import('@vueuse/core')['isDefined']
const isMobile: typeof import('./composables/media')['isMobile']
const isMobile: typeof import('./composable/media')['isMobile']
const isObject: typeof import('./utils/index')['isObject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const lightTheme: typeof import('./composables/settings')['lightTheme']
const lightTheme: typeof import('./stores/settings')['lightTheme']
const logicAnd: typeof import('@vueuse/math')['logicAnd']
const logicNot: typeof import('@vueuse/math')['logicNot']
const logicOr: typeof import('@vueuse/math')['logicOr']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
@@ -72,7 +80,7 @@ declare global {
const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw']
const menuWidth: typeof import('./composables/settings')['menuWidth']
const menuWidth: typeof import('./stores/settings')['menuWidth']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
@@ -94,8 +102,11 @@ declare global {
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const persistentVisibleKeys: typeof import('./composables/storage')['persistentVisibleKeys']
const persistentVisibleKeys: typeof import('./composable/storage')['persistentVisibleKeys']
const pinnedContainers: typeof import('./composable/storage')['pinnedContainers']
const provide: typeof import('vue')['provide']
const provideContainerContext: typeof import('./composable/containerContext')['provideContainerContext']
const provideLocal: typeof import('@vueuse/core')['provideLocal']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
@@ -109,24 +120,25 @@ declare global {
const refDefault: typeof import('@vueuse/core')['refDefault']
const refThrottled: typeof import('@vueuse/core')['refThrottled']
const refWithControl: typeof import('@vueuse/core')['refWithControl']
const releases: typeof import('./stores/releases')['default']
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const search: typeof import('./composables/settings')['search']
const sessionHost: typeof import('./composables/storage')['sessionHost']
const search: typeof import('./stores/settings')['search']
const sessionHost: typeof import('./composable/storage')['sessionHost']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const setTitle: typeof import('./composables/title')['setTitle']
const settings: typeof import('./composables/settings')['settings']
const setTitle: typeof import('./composable/title')['setTitle']
const settings: typeof import('./stores/settings')['settings']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const showAllContainers: typeof import('./composables/settings')['showAllContainers']
const showStd: typeof import('./composables/settings')['showStd']
const showTimestamp: typeof import('./composables/settings')['showTimestamp']
const size: typeof import('./composables/settings')['size']
const smallerScrollbars: typeof import('./composables/settings')['smallerScrollbars']
const softWrap: typeof import('./composables/settings')['softWrap']
const showAllContainers: typeof import('./stores/settings')['showAllContainers']
const showStd: typeof import('./stores/settings')['showStd']
const showTimestamp: typeof import('./stores/settings')['showTimestamp']
const size: typeof import('./stores/settings')['size']
const smallerScrollbars: typeof import('./stores/settings')['smallerScrollbars']
const softWrap: typeof import('./stores/settings')['softWrap']
const storeToRefs: typeof import('pinia')['storeToRefs']
const stripVersion: typeof import('./utils/index')['stripVersion']
const syncRef: typeof import('@vueuse/core')['syncRef']
@@ -148,6 +160,7 @@ declare global {
const unref: typeof import('vue')['unref']
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useAbs: typeof import('@vueuse/math')['useAbs']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
@@ -165,6 +178,7 @@ declare global {
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
const useAverage: typeof import('@vueuse/math')['useAverage']
const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: typeof import('@vueuse/core')['useBattery']
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
@@ -172,10 +186,14 @@ declare global {
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: typeof import('@vueuse/core')['useCached']
const useCeil: typeof import('@vueuse/math')['useCeil']
const useClamp: typeof import('@vueuse/math')['useClamp']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useContainerContext: typeof import('./composable/containerContext')['useContainerContext']
const useContainerStore: typeof import('./stores/container')['useContainerStore']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
@@ -204,11 +222,13 @@ 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']
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
const useFloor: typeof import('@vueuse/math')['useFloor']
const useFocus: typeof import('@vueuse/core')['useFocus']
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
const useFps: typeof import('@vueuse/core')['useFps']
@@ -227,13 +247,16 @@ declare global {
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLink: typeof import('vue-router')['useLink']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useLogStream: typeof import('./composables/eventsource')['useLogStream']
const useLogStream: typeof import('./composable/eventsource')['useLogStream']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMath: typeof import('@vueuse/math')['useMath']
const useMax: typeof import('@vueuse/math')['useMax']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useMin: typeof import('@vueuse/math')['useMin']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
@@ -253,15 +276,19 @@ declare global {
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePrecision: typeof import('@vueuse/math')['usePrecision']
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 useProjection: typeof import('@vueuse/math')['useProjection']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useReleases: typeof import('./stores/releases')['useReleases']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRound: typeof import('@vueuse/math')['useRound']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
@@ -269,7 +296,7 @@ declare global {
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSearchFilter: typeof import('./composables/search')['useSearchFilter']
const useSearchFilter: typeof import('./composable/search')['useSearchFilter']
const useSeoMeta: typeof import('@vueuse/head')['useSeoMeta']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
@@ -281,6 +308,7 @@ declare global {
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSum: typeof import('@vueuse/math')['useSum']
const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
@@ -298,15 +326,17 @@ declare global {
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToNumber: typeof import('@vueuse/core')['useToNumber']
const useToString: typeof import('@vueuse/core')['useToString']
const useToast: typeof import('./composable/toast')['useToast']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useTrunc: typeof import('@vueuse/math')['useTrunc']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
const useVModel: typeof import('@vueuse/core')['useVModel']
const useVModels: typeof import('@vueuse/core')['useVModels']
const useVibrate: typeof import('@vueuse/core')['useVibrate']
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
const useVisibleFilter: typeof import('./composables/visible')['useVisibleFilter']
const useVisibleFilter: typeof import('./composable/visible')['useVisibleFilter']
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
@@ -331,11 +361,12 @@ declare global {
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: typeof import('@vueuse/core')['whenever']
const withBase: typeof import('./stores/config')['withBase']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue'
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
}
// for vue template auto import
import { UnwrapRef } from 'vue'
@@ -348,19 +379,21 @@ declare module 'vue' {
readonly $ref: UnwrapRef<typeof import('vue/macros')['$ref']>
readonly $shallowRef: UnwrapRef<typeof import('vue/macros')['$shallowRef']>
readonly $toRef: UnwrapRef<typeof import('vue/macros')['$toRef']>
readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./composables/settings')['DEFAULT_SETTINGS']>
readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./stores/settings')['DEFAULT_SETTINGS']>
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly collapseNav: UnwrapRef<typeof import('./composables/settings')['collapseNav']>
readonly automaticRedirect: UnwrapRef<typeof import('./stores/settings')['automaticRedirect']>
readonly collapseNav: UnwrapRef<typeof import('./stores/settings')['collapseNav']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
readonly config: UnwrapRef<typeof import('./stores/config')['default']>
readonly containerContext: UnwrapRef<typeof import('./composable/containerContext')['containerContext']>
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
@@ -388,19 +421,20 @@ declare module 'vue' {
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getDeep: UnwrapRef<typeof import('./utils/index')['getDeep']>
readonly globalShowPopup: UnwrapRef<typeof import('./composables/popup')['globalShowPopup']>
readonly globalShowPopup: UnwrapRef<typeof import('./composable/popup')['globalShowPopup']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly hourStyle: UnwrapRef<typeof import('./composables/settings')['hourStyle']>
readonly hourStyle: UnwrapRef<typeof import('./stores/settings')['hourStyle']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isMobile: UnwrapRef<typeof import('./composables/media')['isMobile']>
readonly isMobile: UnwrapRef<typeof import('./composable/media')['isMobile']>
readonly isObject: UnwrapRef<typeof import('./utils/index')['isObject']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly lightTheme: UnwrapRef<typeof import('./composables/settings')['lightTheme']>
readonly lightTheme: UnwrapRef<typeof import('./stores/settings')['lightTheme']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
@@ -408,7 +442,7 @@ declare module 'vue' {
readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly menuWidth: UnwrapRef<typeof import('./composables/settings')['menuWidth']>
readonly menuWidth: UnwrapRef<typeof import('./stores/settings')['menuWidth']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
@@ -430,8 +464,11 @@ declare module 'vue' {
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly persistentVisibleKeys: UnwrapRef<typeof import('./composables/storage')['persistentVisibleKeys']>
readonly persistentVisibleKeys: UnwrapRef<typeof import('./composable/storage')['persistentVisibleKeys']>
readonly pinnedContainers: UnwrapRef<typeof import('./composable/storage')['pinnedContainers']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideContainerContext: UnwrapRef<typeof import('./composable/containerContext')['provideContainerContext']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
@@ -448,21 +485,21 @@ declare module 'vue' {
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly search: UnwrapRef<typeof import('./composables/settings')['search']>
readonly sessionHost: UnwrapRef<typeof import('./composables/storage')['sessionHost']>
readonly search: UnwrapRef<typeof import('./stores/settings')['search']>
readonly sessionHost: UnwrapRef<typeof import('./composable/storage')['sessionHost']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly setTitle: UnwrapRef<typeof import('./composables/title')['setTitle']>
readonly settings: UnwrapRef<typeof import('./composables/settings')['settings']>
readonly setTitle: UnwrapRef<typeof import('./composable/title')['setTitle']>
readonly settings: UnwrapRef<typeof import('./stores/settings')['settings']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly showAllContainers: UnwrapRef<typeof import('./composables/settings')['showAllContainers']>
readonly showStd: UnwrapRef<typeof import('./composables/settings')['showStd']>
readonly showTimestamp: UnwrapRef<typeof import('./composables/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./composables/settings')['size']>
readonly smallerScrollbars: UnwrapRef<typeof import('./composables/settings')['smallerScrollbars']>
readonly softWrap: UnwrapRef<typeof import('./composables/settings')['softWrap']>
readonly showAllContainers: UnwrapRef<typeof import('./stores/settings')['showAllContainers']>
readonly showStd: UnwrapRef<typeof import('./stores/settings')['showStd']>
readonly showTimestamp: UnwrapRef<typeof import('./stores/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./stores/settings')['size']>
readonly smallerScrollbars: UnwrapRef<typeof import('./stores/settings')['smallerScrollbars']>
readonly softWrap: UnwrapRef<typeof import('./stores/settings')['softWrap']>
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
readonly stripVersion: UnwrapRef<typeof import('./utils/index')['stripVersion']>
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
@@ -509,9 +546,11 @@ declare module 'vue' {
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']>
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useContainerContext: UnwrapRef<typeof import('./composable/containerContext')['useContainerContext']>
readonly useContainerStore: UnwrapRef<typeof import('./stores/container')['useContainerStore']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
@@ -540,6 +579,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']>
@@ -563,7 +603,7 @@ declare module 'vue' {
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useLogStream: UnwrapRef<typeof import('./composables/eventsource')['useLogStream']>
readonly useLogStream: UnwrapRef<typeof import('./composable/eventsource')['useLogStream']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
@@ -597,6 +637,7 @@ declare module 'vue' {
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 useReleases: UnwrapRef<typeof import('./stores/releases')['useReleases']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
@@ -605,7 +646,7 @@ declare module 'vue' {
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSearchFilter: UnwrapRef<typeof import('./composables/search')['useSearchFilter']>
readonly useSearchFilter: UnwrapRef<typeof import('./composable/search')['useSearchFilter']>
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
@@ -634,6 +675,7 @@ declare module 'vue' {
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
readonly useToast: UnwrapRef<typeof import('./composable/toast')['useToast']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
@@ -642,7 +684,7 @@ declare module 'vue' {
readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
readonly useVisibleFilter: UnwrapRef<typeof import('./composables/visible')['useVisibleFilter']>
readonly useVisibleFilter: UnwrapRef<typeof import('./composable/visible')['useVisibleFilter']>
readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
@@ -667,6 +709,7 @@ declare module 'vue' {
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
readonly withBase: UnwrapRef<typeof import('./stores/config')['withBase']>
}
}
declare module '@vue/runtime-core' {
@@ -678,19 +721,21 @@ declare module '@vue/runtime-core' {
readonly $ref: UnwrapRef<typeof import('vue/macros')['$ref']>
readonly $shallowRef: UnwrapRef<typeof import('vue/macros')['$shallowRef']>
readonly $toRef: UnwrapRef<typeof import('vue/macros')['$toRef']>
readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./composables/settings')['DEFAULT_SETTINGS']>
readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./stores/settings')['DEFAULT_SETTINGS']>
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly collapseNav: UnwrapRef<typeof import('./composables/settings')['collapseNav']>
readonly automaticRedirect: UnwrapRef<typeof import('./stores/settings')['automaticRedirect']>
readonly collapseNav: UnwrapRef<typeof import('./stores/settings')['collapseNav']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
readonly config: UnwrapRef<typeof import('./stores/config')['default']>
readonly containerContext: UnwrapRef<typeof import('./composable/containerContext')['containerContext']>
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
@@ -718,19 +763,20 @@ declare module '@vue/runtime-core' {
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getDeep: UnwrapRef<typeof import('./utils/index')['getDeep']>
readonly globalShowPopup: UnwrapRef<typeof import('./composables/popup')['globalShowPopup']>
readonly globalShowPopup: UnwrapRef<typeof import('./composable/popup')['globalShowPopup']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly hourStyle: UnwrapRef<typeof import('./composables/settings')['hourStyle']>
readonly hourStyle: UnwrapRef<typeof import('./stores/settings')['hourStyle']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isMobile: UnwrapRef<typeof import('./composables/media')['isMobile']>
readonly isMobile: UnwrapRef<typeof import('./composable/media')['isMobile']>
readonly isObject: UnwrapRef<typeof import('./utils/index')['isObject']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly lightTheme: UnwrapRef<typeof import('./composables/settings')['lightTheme']>
readonly lightTheme: UnwrapRef<typeof import('./stores/settings')['lightTheme']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
@@ -738,7 +784,7 @@ declare module '@vue/runtime-core' {
readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly menuWidth: UnwrapRef<typeof import('./composables/settings')['menuWidth']>
readonly menuWidth: UnwrapRef<typeof import('./stores/settings')['menuWidth']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
@@ -760,8 +806,11 @@ declare module '@vue/runtime-core' {
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly persistentVisibleKeys: UnwrapRef<typeof import('./composables/storage')['persistentVisibleKeys']>
readonly persistentVisibleKeys: UnwrapRef<typeof import('./composable/storage')['persistentVisibleKeys']>
readonly pinnedContainers: UnwrapRef<typeof import('./composable/storage')['pinnedContainers']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideContainerContext: UnwrapRef<typeof import('./composable/containerContext')['provideContainerContext']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
@@ -778,21 +827,21 @@ declare module '@vue/runtime-core' {
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly search: UnwrapRef<typeof import('./composables/settings')['search']>
readonly sessionHost: UnwrapRef<typeof import('./composables/storage')['sessionHost']>
readonly search: UnwrapRef<typeof import('./stores/settings')['search']>
readonly sessionHost: UnwrapRef<typeof import('./composable/storage')['sessionHost']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly setTitle: UnwrapRef<typeof import('./composables/title')['setTitle']>
readonly settings: UnwrapRef<typeof import('./composables/settings')['settings']>
readonly setTitle: UnwrapRef<typeof import('./composable/title')['setTitle']>
readonly settings: UnwrapRef<typeof import('./stores/settings')['settings']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly showAllContainers: UnwrapRef<typeof import('./composables/settings')['showAllContainers']>
readonly showStd: UnwrapRef<typeof import('./composables/settings')['showStd']>
readonly showTimestamp: UnwrapRef<typeof import('./composables/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./composables/settings')['size']>
readonly smallerScrollbars: UnwrapRef<typeof import('./composables/settings')['smallerScrollbars']>
readonly softWrap: UnwrapRef<typeof import('./composables/settings')['softWrap']>
readonly showAllContainers: UnwrapRef<typeof import('./stores/settings')['showAllContainers']>
readonly showStd: UnwrapRef<typeof import('./stores/settings')['showStd']>
readonly showTimestamp: UnwrapRef<typeof import('./stores/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./stores/settings')['size']>
readonly smallerScrollbars: UnwrapRef<typeof import('./stores/settings')['smallerScrollbars']>
readonly softWrap: UnwrapRef<typeof import('./stores/settings')['softWrap']>
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
readonly stripVersion: UnwrapRef<typeof import('./utils/index')['stripVersion']>
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
@@ -839,9 +888,11 @@ declare module '@vue/runtime-core' {
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']>
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useContainerContext: UnwrapRef<typeof import('./composable/containerContext')['useContainerContext']>
readonly useContainerStore: UnwrapRef<typeof import('./stores/container')['useContainerStore']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
@@ -870,6 +921,7 @@ declare module '@vue/runtime-core' {
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']>
@@ -893,7 +945,7 @@ declare module '@vue/runtime-core' {
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useLogStream: UnwrapRef<typeof import('./composables/eventsource')['useLogStream']>
readonly useLogStream: UnwrapRef<typeof import('./composable/eventsource')['useLogStream']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
@@ -927,6 +979,7 @@ declare module '@vue/runtime-core' {
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 useReleases: UnwrapRef<typeof import('./stores/releases')['useReleases']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
@@ -935,7 +988,7 @@ declare module '@vue/runtime-core' {
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSearchFilter: UnwrapRef<typeof import('./composables/search')['useSearchFilter']>
readonly useSearchFilter: UnwrapRef<typeof import('./composable/search')['useSearchFilter']>
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
@@ -964,6 +1017,7 @@ declare module '@vue/runtime-core' {
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
readonly useToast: UnwrapRef<typeof import('./composable/toast')['useToast']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
@@ -972,7 +1026,7 @@ declare module '@vue/runtime-core' {
readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
readonly useVisibleFilter: UnwrapRef<typeof import('./composables/visible')['useVisibleFilter']>
readonly useVisibleFilter: UnwrapRef<typeof import('./composable/visible')['useVisibleFilter']>
readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
@@ -997,5 +1051,6 @@ declare module '@vue/runtime-core' {
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
readonly withBase: UnwrapRef<typeof import('./stores/config')['withBase']>
}
}
+32 -9
View File
@@ -7,25 +7,36 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
BarChart: typeof import('./components/BarChart.vue')['default']
'Carbon:caretDown': typeof import('~icons/carbon/caret-down')['default']
'Carbon:circleSolid': typeof import('~icons/carbon/circle-solid')['default']
'Carbon:information': typeof import('~icons/carbon/information')['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']
'Carbon:warning': typeof import('~icons/carbon/warning')['default']
'Cil:checkCircle': typeof import('~icons/cil/check-circle')['default']
'Cil:circle': typeof import('~icons/cil/circle')['default']
'Cil:columns': typeof import('~icons/cil/columns')['default']
'Cil:findInPage': typeof import('~icons/cil/find-in-page')['default']
'Cil:xCircle': typeof import('~icons/cil/x-circle')['default']
ComplexLogItem: typeof import('./components/LogViewer/ComplexLogItem.vue')['default']
ContainerHealth: typeof import('./components/LogViewer/ContainerHealth.vue')['default']
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']
Dropdown: typeof import('./components/common/Dropdown.vue')['default']
DropdownMenu: typeof import('./components/common/DropdownMenu.vue')['default']
FieldList: typeof import('./components/LogViewer/FieldList.vue')['default']
FuzzySearchModal: typeof import('./components/FuzzySearchModal.vue')['default']
'Ic:sharpFindInPage': typeof import('~icons/ic/sharp-find-in-page')['default']
'Ic:sharpKeyboardReturn': typeof import('~icons/ic/sharp-keyboard-return')['default']
InfiniteLoader: typeof import('./components/InfiniteLoader.vue')['default']
KeyShortcut: typeof import('./components/common/KeyShortcut.vue')['default']
Links: typeof import('./components/Links.vue')['default']
LogActionsToolbar: typeof import('./components/LogViewer/LogActionsToolbar.vue')['default']
LogContainer: typeof import('./components/LogViewer/LogContainer.vue')['default']
LogDate: typeof import('./components/LogViewer/LogDate.vue')['default']
@@ -34,18 +45,28 @@ declare module 'vue' {
LogStd: typeof import('./components/LogViewer/LogStd.vue')['default']
LogViewer: typeof import('./components/LogViewer/LogViewer.vue')['default']
LogViewerWithSource: typeof import('./components/LogViewer/LogViewerWithSource.vue')['default']
'Mdi:dotsVertical': typeof import('~icons/mdi/dots-vertical')['default']
'Mdi:lightChevronDoubleDown': typeof import('~icons/mdi-light/chevron-double-down')['default']
'Mdi:lightChevronLeft': typeof import('~icons/mdi-light/chevron-left')['default']
'Mdi:lightChevronRight': typeof import('~icons/mdi-light/chevron-right')['default']
'Mdi:lightCog': typeof import('~icons/mdi-light/cog')['default']
'Mdi:lightLogout': typeof import('~icons/mdi-light/logout')['default']
'Mdi:lightMagnify': typeof import('~icons/mdi-light/magnify')['default']
'Mdi:announcement': typeof import('~icons/mdi/announcement')['default']
'Mdi:arrowUp': typeof import('~icons/mdi/arrow-up')['default']
'Mdi:check': typeof import('~icons/mdi/check')['default']
'Mdi:chevronDoubleDown': typeof import('~icons/mdi/chevron-double-down')['default']
'Mdi:chevronLeft': typeof import('~icons/mdi/chevron-left')['default']
'Mdi:chevronRight': typeof import('~icons/mdi/chevron-right')['default']
'Mdi:close': typeof import('~icons/mdi/close')['default']
'Mdi:cog': typeof import('~icons/mdi/cog')['default']
'Mdi:hamburgerMenu': typeof import('~icons/mdi/hamburger-menu')['default']
'Mdi:keyboardEsc': typeof import('~icons/mdi/keyboard-esc')['default']
'Mdi:logout': typeof import('~icons/mdi/logout')['default']
'Mdi:magnify': typeof import('~icons/mdi/magnify')['default']
MobileMenu: typeof import('./components/common/MobileMenu.vue')['default']
'Octicon:container24': typeof import('~icons/octicon/container24')['default']
'Octicon:download24': typeof import('~icons/octicon/download24')['default']
'Octicon:trash24': typeof import('~icons/octicon/trash24')['default']
PageWithLinks: typeof import('./components/PageWithLinks.vue')['default']
'Ph:command': typeof import('~icons/ph/command')['default']
'Ph:computerTower': typeof import('~icons/ph/computer-tower')['default']
'Ph:controlBold': typeof import('~icons/ph/control-bold')['default']
Popup: typeof import('./components/Popup.vue')['default']
Releases: typeof import('./components/Releases.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ScrollableView: typeof import('./components/ScrollableView.vue')['default']
@@ -58,6 +79,8 @@ declare module 'vue' {
StatMonitor: typeof import('./components/LogViewer/StatMonitor.vue')['default']
StatSparkline: typeof import('./components/LogViewer/StatSparkline.vue')['default']
Tag: typeof import('./components/common/Tag.vue')['default']
TimedButton: typeof import('./components/common/TimedButton.vue')['default']
Toggle: typeof import('./components/common/Toggle.vue')['default']
ZigZag: typeof import('./components/LogViewer/ZigZag.vue')['default']
}
}
+20
View File
@@ -0,0 +1,20 @@
<template>
<div class="relative">
<div class="bar h-7 origin-left rounded-br rounded-tr bg-primary transition-transform"></div>
<div class="absolute inset-0 flex flex-col justify-center px-2 text-sm">
<slot></slot>
</div>
</div>
</template>
<script lang="ts" setup>
const { value } = defineProps<{ value: number }>();
const minValue = computed(() => Math.min(value, 1));
</script>
<style scoped>
.bar {
transform: scaleX(v-bind(minValue));
}
</style>
+171
View File
@@ -0,0 +1,171 @@
!
<template>
<table class="table table-lg bg-base">
<thead>
<tr :data-direction="direction > 0 ? 'asc' : 'desc'">
<th
v-for="(value, key) in fields"
:key="key"
@click.prevent="sort(key)"
:class="{ 'selected-sort': key === sortField }"
v-show="isVisible(key)"
>
<a class="inline-flex cursor-pointer gap-2 text-sm uppercase">
<span>{{ $t(value.label) }}</span>
<span class="h-4" data-icon>
<mdi:arrow-up />
</span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr v-for="container in paginated" :key="container.id">
<td v-if="isVisible('name')">
<router-link :to="{ name: 'container-id', params: { id: container.id } }" :title="container.name">
{{ container.name }}
</router-link>
</td>
<td v-if="isVisible('host')">{{ container.hostLabel }}</td>
<td v-if="isVisible('state')">{{ container.state }}</td>
<td v-if="isVisible('created')">
<distance-time :date="container.created" strict :suffix="false"></distance-time>
</td>
<td v-if="isVisible('cpu')">
<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>
<div class="p-4 text-center">
<nav class="join" v-if="isPaginated">
<button
v-for="i in totalPages"
class="btn join-item"
:class="{ 'btn-primary': i === currentPage }"
@click="currentPage = i"
>
{{ i }}
</button>
</nav>
</div>
</template>
<script setup lang="ts">
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,
},
host: {
label: "label.host",
sortFunc: (a: Container, b: Container) => a.hostLabel.localeCompare(b.hostLabel) * direction.value,
mobileVisible: false,
},
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="postcss" scoped>
[data-icon] {
display: none;
transition: transform 0.2s ease-in-out;
[data-direction="desc"] & {
transform: rotate(180deg);
}
}
th {
@apply border-b-2 border-base-lighter;
&.selected-sort {
font-weight: bold;
@apply border-primary;
[data-icon] {
display: inline-block;
}
}
}
tbody td {
max-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
a {
@apply hover:text-primary;
}
</style>
-48
View File
@@ -1,48 +0,0 @@
<template>
<div class="dropdown is-hoverable">
<div class="dropdown-trigger">
<slot name="trigger">
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu">
<span class="icon">
<mdi:dots-vertical />
</span>
</button>
</slot>
</div>
<div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-content">
<slot></slot>
</div>
</div>
</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped>
.minimal .button {
background-color: rgba(0, 0, 0, 0);
border: none;
padding: 0.1em;
height: 100%;
& > .icon {
height: 100%;
& > svg {
align-self: flex-start;
height: 0.85em;
}
}
}
.is-top {
& .dropdown-menu {
top: 0;
}
&.is-last .dropdown-menu {
top: -30px;
}
}
</style>
+58 -82
View File
@@ -1,56 +1,64 @@
<template>
<div class="panel">
<o-autocomplete
ref="autocomplete"
v-model="query"
:placeholder="$t('placeholder.search-containers')"
open-on-focus
keep-first
expanded
:data="data"
@select="selected"
>
<template #default="{ option: item }">
<div class="media">
<div class="media-left">
<span class="icon is-small" :class="item.state">
<octicon:container-24 />
</span>
<div class="dropdown dropdown-open w-full">
<div class="input input-primary flex h-auto items-center">
<mdi:magnify class="flex h-8 w-8" />
<input
tabindex="0"
class="input input-ghost input-lg flex-1 px-1"
ref="input"
@keyup.down="selectedIndex = Math.min(selectedIndex + 1, data.length - 1)"
@keyup.up="selectedIndex = Math.max(selectedIndex - 1, 0)"
@keyup.enter.exact="selected(data[selectedIndex])"
@keyup.alt.enter="addColumn(data[selectedIndex])"
v-model="query"
:placeholder="$t('placeholder.search-containers')"
/>
<mdi:keyboard-esc class="flex" />
</div>
<ul tabindex="0" class="menu dropdown-content rounded-box !relative mt-2 w-full bg-base-lighter p-2">
<li v-for="(item, index) in data">
<a
class="grid auto-cols-max grid-cols-[min-content,auto] gap-2 py-4"
@click.prevent="selected(item)"
@mouseenter="selectedIndex = index"
:class="index === selectedIndex ? 'focus' : ''"
>
<div :class="{ 'text-primary': item.state === 'running' }">
<octicon:container-24 />
</div>
<div class="media-content">{{ item.host }} / {{ item.name }}</div>
<div class="media-right">
<span
class="icon is-small column-icon"
@click.stop.prevent="addColumn(item)"
:title="$t('tooltip.pin-column')"
>
<cil:columns />
</span>
<div class="truncate">
<span class="font-light">{{ item.host }}</span> / {{ item.name }}
</div>
</div>
</template>
</o-autocomplete>
<distance-time :date="item.created" class="ml-auto text-xs font-light" />
<a @click.stop.prevent="addColumn(item)" :title="$t('tooltip.pin-column')" class="hover:text-secondary">
<ic:sharp-keyboard-return v-if="index === selectedIndex" />
<cil:columns v-else />
</a>
</a>
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
import { useFuse } from "@vueuse/integrations/useFuse";
const { maxResults: resultLimit = 20 } = defineProps<{
const { maxResults: resultLimit = 5 } = defineProps<{
maxResults?: number;
}>();
const close = defineEmit();
const query = ref("");
const autocomplete = ref<HTMLElement>();
const input = ref<HTMLInputElement>();
const selectedIndex = ref(0);
const router = useRouter();
const store = useContainerStore();
const { containers } = storeToRefs(store);
const list = computed(() => {
return containers.value.map(({ id, created, name, state, host }) => {
return containers.value.map(({ id, created, name, state, hostLabel: host }) => {
return {
id,
created,
@@ -62,7 +70,11 @@ const list = computed(() => {
});
const { results } = useFuse(query, list, {
fuseOptions: { keys: ["name"], includeScore: true },
fuseOptions: {
keys: ["name", "host"],
includeScore: true,
useExtendedSearch: true,
},
resultLimit,
matchAllWhenSearchEmpty: true,
});
@@ -82,62 +94,26 @@ const data = computed(() => {
return 0;
}
})
.map(({ item }) => item);
.map(({ item }) => item)
.slice(0, resultLimit);
});
watchOnce(autocomplete, () => autocomplete.value?.focus());
watch(query, (data) => {
if (data.length > 0) {
selectedIndex.value = 0;
}
});
onMounted(() => input.value?.focus());
function selected({ id }: { id: string }) {
router.push({ name: "container-id", params: { id } });
close();
}
function addColumn(container: Container) {
function addColumn(container: { id: string }) {
store.appendActiveContainer(container);
close();
}
</script>
<style lang="scss" scoped>
.panel {
min-height: 400px;
width: 580px;
}
@media screen and (max-width: 768px) {
.panel {
min-height: 200px;
width: auto;
margin-left: 0.25rem !important;
margin-right: 0.25rem !important;
}
}
.running {
color: var(--primary-color);
}
.exited {
color: var(--scheme-main-ter);
}
.column-icon {
&:hover {
color: var(--secondary-color);
}
}
:deep(a.dropdown-item) {
padding-right: 1em;
.media-right {
visibility: hidden;
}
&:hover .media-right {
visibility: visible;
}
}
.icon {
vertical-align: middle;
}
</style>
<style scoped lang="postcss"></style>
+4 -46
View File
@@ -1,16 +1,12 @@
<template>
<div ref="root" class="infinte-loader">
<div class="spinner" v-show="isLoading">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
<div ref="root" class="flex min-h-[1px] justify-center">
<span class="loading loading-bars loading-md mt-4 text-primary" v-show="isLoading"></span>
</div>
</template>
<script lang="ts" setup>
const { onLoadMore = () => {}, enabled } = defineProps<{
onLoadMore: () => void;
const { onLoadMore = () => Promise.resolve(), enabled } = defineProps<{
onLoadMore: () => Promise<void>;
enabled: boolean;
}>();
@@ -33,41 +29,3 @@ const observer = new IntersectionObserver(async (entries) => {
onMounted(() => observer.observe(root.value!));
onUnmounted(() => observer.disconnect());
</script>
<style scoped lang="scss">
.infinte-loader {
min-height: 1px;
}
.spinner {
margin: 10px auto 0;
width: 70px;
text-align: center;
& > div {
width: 12px;
height: 12px;
background-color: var(--primary-color);
border-radius: 100%;
display: inline-block;
animation: sk-bouncedelay 0.8s infinite ease-in-out both;
}
& .bounce1 {
animation-delay: -0.32s;
}
& .bounce2 {
animation-delay: -0.16s;
}
}
@keyframes sk-bouncedelay {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
</style>
+63
View File
@@ -0,0 +1,63 @@
<template>
<div class="flex items-center justify-end gap-4">
<template v-if="config.pages">
<router-link
:to="{ name: 'content-id', params: { id: page.id } }"
:title="page.title"
v-for="page in config.pages"
:key="page.id"
class="link-primary"
>
{{ page.title }}
</router-link>
</template>
<dropdown class="dropdown-end" @closed="latestTag = latest?.tag ?? config.version">
<template #trigger>
<mdi:announcement class="h-6 w-6 -rotate-12" />
<span
class="absolute right-px top-0 h-2 w-2 rounded-full bg-red"
v-if="hasUpdate && latestTag != latest?.tag"
></span>
</template>
<template #content>
<div class="w-72">
<releases />
</div>
</template>
</dropdown>
<dropdown class="dropdown-end" v-if="config.user">
<template #trigger>
<img class="h-6 w-6 max-w-none rounded-full p-px ring-1 ring-base-content/60" :src="config.user.avatar" />
</template>
<template #content>
<div class="p-2">
<div class="font-bold">
{{ config.user.name }}
</div>
<div class="text-sm font-light">
{{ config.user.email }}
</div>
</div>
<ul class="menu mt-4 p-0">
<li v-if="config.authProvider === 'simple'">
<button @click.prevent="logout()" class="p-2 text-primary">{{ $t("button.logout") }}</button>
</li>
</ul>
</template>
</dropdown>
</div>
</template>
<script lang="ts" setup>
async function logout() {
await fetch(withBase("/api/token"), {
method: "DELETE",
});
location.reload();
}
const { hasUpdate, latest } = useReleases();
const latestTag = useStorage("DOZZLE_LATEST_TAG", config.version);
</script>
+16 -29
View File
@@ -1,25 +1,24 @@
<template>
<div class="columns is-1 is-variable is-mobile">
<div class="column is-narrow" v-if="showStd">
<div class="flex gap-x-2">
<div v-if="showStd">
<log-std :std="logEntry.std"></log-std>
</div>
<div class="column is-narrow" v-if="showTimestamp">
<div v-if="showTimestamp">
<log-date :date="logEntry.date"></log-date>
</div>
<div class="column is-narrow is-flex">
<div class="flex">
<log-level :level="logEntry.level"></log-level>
</div>
<div class="column">
<ul class="fields" :class="{ expanded }" @click="expandToggle()">
<li v-for="(value, name) in validValues">
<span class="has-text-grey">{{ name }}=</span
><span class="has-text-weight-bold" v-if="value === null">&lt;null&gt;</span>
<div>
<ul class="fields cursor-pointer space-x-4" :class="{ expanded }" @click="expandToggle()">
<li v-for="(value, name) in validValues" class="inline-block">
<span class="text-light">{{ name }}=</span><span class="font-bold" v-if="value === null">&lt;null&gt;</span>
<template v-else-if="Array.isArray(value)">
<span class="has-text-weight-bold" v-html="markSearch(JSON.stringify(value))"> </span>
<span class="font-bold" v-html="markSearch(JSON.stringify(value))"> </span>
</template>
<span class="has-text-weight-bold" v-html="markSearch(value)" v-else></span>
<span class="font-bold" v-html="markSearch(value)" v-else></span>
</li>
<li class="has-text-grey" v-if="Object.keys(validValues).length === 0">all values are hidden</li>
<li class="text-light" v-if="Object.keys(validValues).length === 0">all values are hidden</li>
</ul>
<field-list :fields="logEntry.unfilteredMessage" :expanded="expanded" :visible-keys="visibleKeys"></field-list>
</div>
@@ -42,19 +41,15 @@ const validValues = computed(() => {
});
</script>
<style lang="scss" scoped>
<style lang="postcss" scoped>
.text-light {
@apply text-base-content/70;
}
.fields {
display: inline-block;
list-style: none;
&:hover {
cursor: pointer;
&::after {
content: "expand json";
color: var(--secondary-color);
display: inline-block;
margin-left: 0.5em;
@apply ml-2 inline-block text-secondary;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
}
@@ -64,13 +59,5 @@ const validValues = computed(() => {
content: "collapse json";
}
}
li {
display: inline-block;
& + li {
margin-left: 1em;
}
}
}
</style>
@@ -1,5 +1,5 @@
<template>
<div class="icon is-small health" :health="health" v-if="health" :title="health">
<div class="inline-flex h-4 w-4" :health="health" v-if="health" :title="health">
<cil:check-circle v-if="health == 'healthy'" />
<cil:x-circle v-else-if="health == 'unhealthy'" />
<cil:circle v-else />
@@ -14,14 +14,12 @@ defineProps<{
}>();
</script>
<style lang="scss" scoped>
.health {
&[health="unhealthy"] {
color: var(--red-color);
}
<style lang="postcss" scoped>
[health="unhealthy"] {
@apply text-red;
}
&[health="healthy"] {
color: var(--green-color);
}
[health="healthy"] {
@apply text-green;
}
</style>
@@ -1,17 +1,17 @@
<template>
<div>
<span class="has-text-weight-light"> RUNNING </span>
<span class="has-text-weight-semibold">
<span class="font-light capitalize"> RUNNING </span>
<span class="font-semibold">
<distance-time :date="container.created" strict :suffix="false"></distance-time>
</span>
</div>
<div>
<span class="has-text-weight-light"> LOAD </span>
<span class="has-text-weight-semibold"> {{ container.getLastStat().snapshot.cpu }}% </span>
<span class="font-light capitalize"> Load </span>
<span class="font-semibold"> {{ container.stat.cpu }}% </span>
</div>
<div>
<span class="has-text-weight-light"> MEM </span>
<span class="has-text-weight-semibold"> {{ formatBytes(container.getLastStat().snapshot.memoryUsage) }} </span>
<span class="font-light capitalize"> MEM </span>
<span class="font-semibold"> {{ formatBytes(container.stat.memoryUsage) }} </span>
</div>
</template>
@@ -22,5 +22,3 @@ const { container } = defineProps<{
container: Container;
}>();
</script>
<style lang="scss" scoped></style>
+10 -44
View File
@@ -1,43 +1,30 @@
<template>
<div class="is-size-7 is-uppercase columns is-marginless is-mobile is-vcentered" v-if="container.stat">
<stat-monitor
class="column is-narrow"
:data="memoryData"
label="mem"
:stat-value="formatBytes(unref(container.stat).memoryUsage)"
></stat-monitor>
<stat-monitor
class="column is-narrow"
:data="cpuData"
label="load"
:stat-value="unref(container.stat).cpu + '%'"
></stat-monitor>
<div class="flex gap-4" v-if="container.stat">
<stat-monitor :data="memoryData" label="mem" :stat-value="formatBytes(unref(container.stat).memoryUsage)" />
<stat-monitor :data="cpuData" label="load" :stat-value="Math.max(0, unref(container.stat).cpu) + '%'" />
</div>
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
import { type ComputedRef } from "vue";
const container = inject("container") as ComputedRef<Container>;
const { container } = useContainerContext();
const cpuData = computedWithControl(
() => container.value.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 + "%",
y: Math.max(0, stat.snapshot.cpu),
value: Math.max(0, 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,
@@ -47,24 +34,3 @@ const memoryData = computedWithControl(
},
);
</script>
<style lang="scss" scoped>
.has-border {
border: 1px solid var(--primary-color);
border-radius: 3px;
padding: 1px 1px 0 1px;
display: flex;
overflow: hidden;
padding-top: 0.25em;
}
.has-background-body-color {
background-color: var(--body-background-color);
}
.is-top-left {
position: absolute;
top: 0;
left: 0.75em;
}
</style>
+30 -17
View File
@@ -1,24 +1,37 @@
<template>
<div class="columns is-marginless has-text-weight-bold is-family-monospace">
<div class="column is-narrow is-hidden-mobile" v-if="container.health">
<container-health :health="container.health"></container-health>
</div>
<div class="column is-ellipsis">
{{ container.name }}<span v-if="container.isSwarm">{{ container.swarmId }}</span>
<tag class="is-hidden-mobile">{{ container.image.replace(/@sha.*/, "") }}</tag>
<div class="flex flex-1 items-center gap-2 truncate">
<container-health :health="container.health" v-if="container.health"></container-health>
<div class="inline-flex font-mono text-sm">
<div v-if="config.hosts.length > 1" class="mobile-hidden font-thin">
{{ container.hostLabel }}<span class="mx-2">/</span>
</div>
<div class="font-semibold">{{ container.name }}</div>
<div
class="mobile-hidden max-w-[1.5em] truncate transition-[max-width] hover:max-w-[400px]"
v-if="container.isSwarm"
>
{{ container.swarmId }}
</div>
</div>
<tag class="mobile-hidden font-mono" size="small">{{ container.image.replace(/@sha.*/, "") }}</tag>
<label class="swap swap-rotate">
<input type="checkbox" v-model="pinned" />
<carbon:star-filled class="swap-on text-secondary" />
<carbon:star class="swap-off" />
</label>
</div>
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
import { type ComputedRef } from "vue";
const container = inject("container") as ComputedRef<Container>;
const { container } = useContainerContext();
const pinned = computed({
get: () => pinnedContainers.value.has(container.value.storageKey),
set: (value) => {
if (value) {
pinnedContainers.value.add(container.value.storageKey);
} else {
pinnedContainers.value.delete(container.value.storageKey);
}
},
});
</script>
<style lang="scss" scoped>
.icon {
vertical-align: middle;
}
</style>
@@ -1,24 +1,73 @@
<template>
<span class="text" :data-event="logEntry.event" v-html="logEntry.message"></span>
<div class="flex-1 font-sans text-[0.9rem]">
<span class="whitespace-pre-wrap" :data-event="logEntry.event" v-html="logEntry.message"></span>
<div
class="alert alert-info mt-8 w-auto text-[1rem] md:mx-auto md:w-1/2"
v-if="nextContainer && logEntry.event === 'container-stopped'"
>
<carbon:information class="h-6 w-6 shrink-0 stroke-current" />
<div>
<h3 class="text-lg font-bold">{{ $t("alert.similar-container-found.title") }}</h3>
{{ $t("alert.similar-container-found.message", { containerId: nextContainer.id }) }}
</div>
<div>
<TimedButton v-if="automaticRedirect" class="btn-primary btn-sm" @finished="redirectNow()">Cancel</TimedButton>
<router-link
:to="{ name: 'container-id', params: { id: nextContainer.id } }"
class="btn btn-primary btn-sm"
v-else
>
Redirect
</router-link>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { DockerEventLogEntry } from "@/models/LogEntry";
const router = useRouter();
const { showToast } = useToast();
const { t } = useI18n();
defineProps<{
const { logEntry } = defineProps<{
logEntry: DockerEventLogEntry;
}>();
const store = useContainerStore();
const { containers } = storeToRefs(store);
const { container } = useContainerContext();
const nextContainer = computed(
() =>
containers.value
.filter(
(c) =>
c.host === container.value.host &&
c.created > logEntry.date &&
c.name === container.value.name &&
c.state === "running",
)
.toSorted((a, b) => +a.created - +b.created)[0],
);
function redirectNow() {
showToast(
{
title: t("alert.redirected.title"),
message: t("alert.redirected.message", { containerId: nextContainer.value.id }),
type: "info",
},
{ expire: 5000 },
);
router.push({ name: "container-id", params: { id: nextContainer.value.id } });
}
</script>
<style lang="scss" scoped>
span {
&[data-event="container-stopped"] {
color: #f14668;
}
&[data-event="container-started"] {
color: hsl(141, 53%, 53%);
}
&.text {
white-space: pre-wrap;
}
<style lang="postcss" scoped>
[data-event="container-stopped"] {
@apply text-red;
}
[data-event="container-started"] {
@apply text-green;
}
</style>
+15 -12
View File
@@ -1,8 +1,8 @@
<template>
<ul v-if="expanded" ref="root">
<ul v-if="expanded" ref="root" class="ml-8">
<li v-for="(value, name) in fields">
<template v-if="isObject(value)">
<span class="has-text-grey">{{ name }}=</span>
<span class="text-light">{{ name }}=</span>
<field-list
:fields="value"
:parent-key="parentKey.concat(name)"
@@ -11,17 +11,20 @@
></field-list>
</template>
<template v-else-if="Array.isArray(value)">
<a @click="toggleField(name)"> {{ hasField(name) ? "remove" : "add" }}&nbsp;</a>
<span class="has-text-grey">{{ name }}=</span>[
<span class="has-text-weight-bold" v-for="(item, index) in value">
{{ item }}
<span v-if="index !== value.length - 1">,</span>
<a @click="toggleField(name)" class="link-primary cursor-pointer">
{{ hasField(name) ? "remove" : "add" }}&nbsp;</a
>
<span class="text-light">{{ name }}=</span>[
<span class="font-bold" v-for="(item, index) in value">
<span v-html="JSON.stringify(item)"></span><span v-if="index !== value.length - 1">,</span>
</span>
]
</template>
<template v-else>
<a @click="toggleField(name)"> {{ hasField(name) ? "remove" : "add" }}&nbsp;</a>
<span class="has-text-grey">{{ name }}=</span><span class="has-text-weight-bold" v-html="value"></span>
<a @click="toggleField(name)" class="link-primary cursor-pointer">
{{ hasField(name) ? "remove" : "add" }}&nbsp;</a
>
<span class="text-light">{{ name }}=</span><span class="font-bold" v-html="value"></span>
</template>
</li>
</ul>
@@ -69,8 +72,8 @@ function fieldIndex(field: string) {
}
</script>
<style lang="scss" scoped>
ul {
margin-left: 2em;
<style scoped lang="postcss">
.text-light {
@apply text-base-content/70;
}
</style>
+67 -113
View File
@@ -1,135 +1,89 @@
<template>
<dropdown-menu class="is-right">
<template #trigger>
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu">
<span class="icon">
<carbon:circle-solid class="is-red is-small" v-if="streamConfig.stderr" />
<carbon:circle-solid class="is-blue is-small" v-if="streamConfig.stdout" />
</span>
</button>
</template>
<a class="dropdown-item" @click="clear()">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<octicon:trash-24 />
</div>
</div>
<div class="level-right">
<div class="level-item">{{ $t("toolbar.clear") }}</div>
</div>
</div>
</a>
<a class="dropdown-item" :href="`${base}/api/logs/download/${sessionHost}/${container.id}`">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<octicon:download-24 />
</div>
</div>
<div class="level-right">
<div class="level-item">{{ $t("toolbar.download") }}</div>
</div>
</div>
</a>
<hr class="dropdown-divider" />
<a class="dropdown-item" @click="showSearch = true">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<mdi:light-magnify />
</div>
</div>
<div class="level-right">
<div class="level-item">{{ $t("toolbar.search") }}</div>
</div>
</div>
</a>
<hr class="dropdown-divider" />
<a
class="dropdown-item"
@click="
streamConfig.stdout = true;
streamConfig.stderr = true;
"
>
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<div class="dropdown dropdown-end dropdown-hover">
<label tabindex="0" class="btn btn-ghost btn-sm gap-0.5 px-2">
<carbon:circle-solid class="w-2.5 text-red" v-if="streamConfig.stderr" />
<carbon:circle-solid class="w-2.5 text-blue" v-if="streamConfig.stdout" />
</label>
<ul tabindex="0" class="menu dropdown-content rounded-box z-50 w-52 bg-base p-1 shadow">
<li>
<a @click.prevent="clear()">
<octicon:trash-24 /> {{ $t("toolbar.clear") }}
<key-shortcut char="k" :modifiers="['shift', 'meta']"></key-shortcut>
</a>
</li>
<li>
<a :href="`${base}/api/logs/download/${container.host}/${container.id}`" download>
<octicon:download-24 /> {{ $t("toolbar.download") }}
</a>
</li>
<li>
<a @click.prevent="showSearch = true">
<mdi:magnify /> {{ $t("toolbar.search") }}
<key-shortcut char="f"></key-shortcut>
</a>
</li>
<li class="line"></li>
<li>
<a
@click="
streamConfig.stdout = true;
streamConfig.stderr = true;
"
>
<div class="flex h-4 w-4 gap-0.5">
<template v-if="streamConfig.stderr && streamConfig.stdout">
<carbon:circle-solid class="is-red is-small" />
<carbon:circle-solid class="is-blue is-small" />
<carbon:circle-solid class="w-2 text-red" />
<carbon:circle-solid class="w-2 text-blue" />
</template>
</div>
</div>
<div class="level-right">
{{ $t("toolbar.show-all") }}
</div>
</div>
</a>
<a
class="dropdown-item"
@click="
streamConfig.stdout = true;
streamConfig.stderr = false;
"
>
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<carbon:circle-solid class="is-blue is-small" v-if="!streamConfig.stderr && streamConfig.stdout" />
</a>
</li>
<li>
<a
@click="
streamConfig.stdout = true;
streamConfig.stderr = false;
"
>
<div class="flex h-4 w-4 flex-col gap-1">
<carbon:circle-solid class="w-2 text-blue" v-if="!streamConfig.stderr && streamConfig.stdout" />
</div>
</div>
<div class="level-right">
{{ $t("toolbar.show", { std: "STDOUT" }) }}
</div>
</div>
</a>
<a
class="dropdown-item"
@click="
streamConfig.stdout = false;
streamConfig.stderr = true;
"
>
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<carbon:circle-solid class="is-red is-small" v-if="streamConfig.stderr && !streamConfig.stdout" />
</a>
</li>
<li>
<a
@click="
streamConfig.stdout = false;
streamConfig.stderr = true;
"
>
<div class="flex h-4 w-4 flex-col gap-1">
<carbon:circle-solid class="w-2 text-red" v-if="streamConfig.stderr && !streamConfig.stdout" />
</div>
</div>
<div class="level-right">
<div class="level-item">
{{ $t("toolbar.show", { std: "STDERR" }) }}
</div>
</div>
</div>
</a>
</dropdown-menu>
{{ $t("toolbar.show", { std: "STDERR" }) }}
</a>
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import { type ComputedRef } from "vue";
import { Container } from "@/models/Container";
const { showSearch } = useSearchFilter();
const { base } = config;
const clear = defineEmit();
const container = inject("container") as ComputedRef<Container>;
const streamConfig = inject("stream-config") as { stdout: boolean; stderr: boolean };
const { container, streamConfig } = useContainerContext();
</script>
<style lang="scss" scoped>
.level-left .level-item {
width: 2.2em;
align-items: center;
margin-right: 0.5em;
<style scoped lang="postcss">
li.line {
@apply h-px bg-base-content/20;
}
.is-small {
width: 0.6em;
a {
@apply whitespace-nowrap;
}
</style>
+15 -33
View File
@@ -1,20 +1,14 @@
<template>
<scrollable-view :scrollable="scrollable" v-if="container">
<template #header v-if="showTitle">
<div class="mr-0 columns is-mobile is-vcentered is-marginless has-boxshadow">
<div class="column is-clipped is-paddingless">
<container-title @close="$emit('close')" />
</div>
<div class="column is-narrow is-paddingless">
<container-stat />
</div>
<div class="mx-2 flex items-center gap-2">
<container-title @close="$emit('close')" />
<container-stat class="ml-auto" />
<div class="mr-2 column is-narrow is-paddingless is-hidden-mobile">
<log-actions-toolbar @clear="onClearClicked()" />
</div>
<div class="mr-2 column is-narrow is-paddingless" v-if="closable">
<button class="delete is-medium" @click="close()"></button>
</div>
<log-actions-toolbar @clear="onClearClicked()" class="mobile-hidden" />
<a class="btn btn-circle btn-xs" @click="close()" v-if="closable">
<mdi:close />
</a>
</div>
</template>
<template #default="{ setLoading }">
@@ -41,31 +35,19 @@ const {
const close = defineEmit();
const store = useContainerStore();
const container = store.currentContainer($$(id));
const config = reactive({ stdout: true, stderr: true });
provide("container", container);
provide("stream-config", config);
provideContainerContext(container);
const viewer = ref<InstanceType<typeof LogViewerWithSource>>();
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);
}
&:hover {
opacity: 1;
}
}
</style>
+3 -12
View File
@@ -1,19 +1,10 @@
<template>
<date-time :date="date" class="date"></date-time>
<tag size="small">
<date-time :date="date" class="whitespace-nowrap text-blue"></date-time>
</tag>
</template>
<script lang="ts" setup>
defineProps<{
date: Date;
}>();
</script>
<style lang="scss" scoped>
.date {
padding-left: 5px;
padding-right: 5px;
border-radius: 3px;
white-space: nowrap;
background-color: var(--scheme-main-ter);
color: #258ccd;
}
</style>
@@ -4,15 +4,17 @@ import { createTestingPinia } from "@pinia/testing";
import EventSource, { sources } from "eventsourcemock";
import LogEventSource from "./LogEventSource.vue";
import LogViewer from "./LogViewer.vue";
import { settings } from "../../composables/settings";
import { useSearchFilter } from "@/composables/search";
import { settings } from "@/stores/settings";
import { useSearchFilter } from "@/composable/search";
import { vi, describe, expect, beforeEach, test, afterEach } from "vitest";
import { computed, nextTick } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import { containerContext } from "@/composable/containerContext";
vi.mock("@/stores/config", () => ({
__esModule: true,
default: { base: "", hosts: ["localhost"] },
default: { base: "", hosts: [{ name: "localhost", id: "localhost" }] },
withBase: (path: string) => path,
}));
/**
@@ -71,8 +73,10 @@ describe("<LogEventSource />", () => {
LogViewer,
},
provide: {
container: computed(() => ({ id: "abc", image: "test:v123", host: "localhost" })),
"stream-config": reactive({ stdout: true, stderr: true }),
[containerContext as symbol]: {
container: computed(() => ({ id: "abc", image: "test:v123", host: "localhost" })),
streamConfig: reactive({ stdout: true, stderr: true }),
},
scrollingPaused: computed(() => false),
},
},
@@ -85,7 +89,7 @@ describe("<LogEventSource />", () => {
});
}
const sourceUrl = "/api/logs/stream/localhost/abc?lastEventId=&stdout=1&stderr=1";
const sourceUrl = "/api/logs/stream/localhost/abc?stdout=1&stderr=1";
test("renders correctly", async () => {
const wrapper = createLogEventSource();
@@ -4,14 +4,9 @@
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
import { type ComputedRef } from "vue";
const loadingMore = defineEmit<[value: boolean]>();
const container = inject("container") as ComputedRef<Container>;
const config = inject("stream-config") as { stdout: boolean; stderr: boolean };
const { messages, loadOlderLogs } = useLogStream(container, config);
const { messages, loadOlderLogs } = useLogStream();
const beforeLoading = () => loadingMore(true);
const afterLoading = () => loadingMore(false);
+35 -44
View File
@@ -1,5 +1,5 @@
<template>
<div :class="level" :data-position="position"></div>
<div :data-level="level" :data-position="position" class="mt-1.5 h-2.5 w-2.5 flex-none rounded-lg"></div>
</template>
<script lang="ts" setup>
import { Position } from "@/models/LogEntry";
@@ -10,54 +10,45 @@ defineProps<{
}>();
</script>
<style lang="scss" scoped>
div {
display: inline-block;
width: 0.7em;
height: 0.7em;
border-radius: 0.5em;
align-self: auto;
margin-top: 0.4em;
<style lang="postcss" scoped>
[data-position="start"] {
border-radius: 0.5em 0.5em 0 0;
height: 70%;
margin-bottom: -0.4em;
margin-top: auto;
align-self: flex-end;
}
&[data-position="start"] {
border-radius: 0.5em 0.5em 0 0;
height: 70%;
margin-bottom: -0.2em;
margin-top: auto;
align-self: flex-end;
}
[data-position="middle"] {
border-radius: 0;
height: auto;
margin: -0.4em 0;
align-self: stretch;
}
&[data-position="middle"] {
border-radius: 0;
height: auto;
margin: -0.2em 0;
align-self: auto;
}
[data-position="end"] {
border-radius: 0 0 0.5em 0.5em;
height: 70%;
margin-top: -0.4em;
align-self: flex-start;
}
&[data-position="end"] {
border-radius: 0 0 0.5em 0.5em;
height: 70%;
margin-top: -0.2em;
align-self: flex-start;
}
[data-level="debug"],
[data-level="trace"] {
@apply bg-purple;
}
&.debug,
&.trace {
background-color: var(--purple-color);
}
[data-level="info"] {
@apply bg-green;
}
&.info {
background-color: var(--green-color);
}
[data-level="error"],
[data-level="fatal"] {
@apply bg-red;
}
&.error,
&.fatal {
background-color: var(--red-color);
}
&.warn,
&.warning {
background-color: var(--orange-color);
}
[data-level="warn"],
[data-level="warning"] {
@apply bg-orange;
}
</style>
+6 -8
View File
@@ -12,14 +12,12 @@ defineProps<{
}>();
</script>
<style lang="scss" scoped>
.tag {
&[std="stdout"] {
color: var(--blue-color);
}
<style lang="postcss" scoped>
[std="stdout"] {
@apply text-blue;
}
&[std="stderr"] {
color: var(--red-color);
}
[std="stderr"] {
@apply text-red;
}
</style>
+26 -47
View File
@@ -1,43 +1,38 @@
<template>
<ul class="events" ref="events" :class="{ 'disable-wrap': !softWrap, [size]: true }">
<ul class="events group py-4" :class="{ 'disable-wrap': !softWrap, [size]: true }">
<li
v-for="(item, index) in filtered"
v-for="item in filtered"
:key="item.id"
:data-key="item.id"
:class="{ selected: toRaw(item) === toRaw(lastSelectedItem) }"
:class="{ 'border border-secondary': toRaw(item) === toRaw(lastSelectedItem) }"
>
<div class="line-options" v-show="isSearching()">
<dropdown-menu :class="{ 'is-last': index === filtered.length - 1 }" class="is-top minimal">
<a class="dropdown-item" @click="handleJumpLineSelected($event, item)" :href="`#${item.id}`">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<cil:find-in-page class="mr-4" />
</div>
</div>
<div class="level-right">
<div class="level-item">Jump to Context</div>
</div>
</div>
</a>
</dropdown-menu>
</div>
<component :is="item.getComponent()" :log-entry="item" :visible-keys="visibleKeys.value"></component>
<a
class="jump-context tooltip tooltip-right tooltip-primary"
v-if="isSearching()"
data-tip="Jump to Context"
@click="handleJumpLineSelected($event, item)"
:href="`#${item.id}`"
>
<ic:sharp-find-in-page />
</a>
<component :is="item.getComponent()" :log-entry="item" :visible-keys="visibleKeys.value" />
</li>
</ul>
</template>
<script lang="ts" setup>
import { type ComputedRef, toRaw } from "vue";
import { toRaw } from "vue";
import { useRouteHash } from "@vueuse/router";
import { Container } from "@/models/Container";
import { type JSONObject, LogEntry } from "@/models/LogEntry";
const props = defineProps<{
messages: LogEntry<string | JSONObject>[];
}>();
let visibleKeys = persistentVisibleKeys(inject("container") as ComputedRef<Container>);
const { container } = useContainerContext();
let visibleKeys = persistentVisibleKeys(container);
const { filteredPayload } = useVisibleFilter(visibleKeys);
const { filteredMessages, resetSearch, isSearching } = useSearchFilter();
@@ -46,7 +41,6 @@ const { messages } = toRefs(props);
const visible = filteredPayload(messages);
const filtered = filteredMessages(visible);
const events = ref<HTMLElement>();
let lastSelectedItem: LogEntry<string | JSONObject> | undefined = $ref(undefined);
function handleJumpLineSelected(e: Event, item: LogEntry<string | JSONObject>) {
@@ -65,9 +59,8 @@ watch(
{ immediate: true, flush: "post" },
);
</script>
<style scoped lang="scss">
<style scoped lang="postcss">
.events {
padding: 1em 0;
font-family:
ui-monospace,
SFMono-Regular,
@@ -78,41 +71,27 @@ watch(
Menlo,
monospace;
& > li {
display: flex;
word-wrap: break-word;
padding: 0.2em 1em;
> li {
@apply flex break-words px-4 py-1 last:snap-end odd:bg-gray-400/[0.07];
&:last-child {
scroll-snap-align: end;
scroll-margin-block-end: 5rem;
}
&:nth-child(odd) {
background-color: rgba(125, 125, 125, 0.08);
}
&.selected {
border: 1px var(--secondary-color) solid;
}
& > .line-options {
display: flex;
flex-direction: row-reverse;
margin-right: 1em;
.jump-context {
@apply mr-2 flex items-center font-sans text-secondary;
}
}
&.small {
font-size: 60%;
@apply text-[0.7em];
}
&.medium {
font-size: 80%;
@apply text-[0.8em];
}
&.large {
font-size: 120%;
@apply text-lg;
}
}
</style>
+5 -23
View File
@@ -1,15 +1,9 @@
<template>
<div class="columns is-1 is-variable is-mobile">
<div class="column is-narrow" v-if="showStd">
<log-std :std="logEntry.std"></log-std>
</div>
<div class="column is-narrow" v-if="showTimestamp">
<log-date :date="logEntry.date"></log-date>
</div>
<div class="column is-narrow is-flex">
<log-level :level="logEntry.level" :position="logEntry.position"></log-level>
</div>
<div class="text column" v-html="colorize(logEntry.message)"></div>
<div class="flex items-start gap-x-2">
<log-std :std="logEntry.std" v-if="showStd" />
<log-date :date="logEntry.date" v-if="showTimestamp" />
<log-level class="flex" :level="logEntry.level" :position="logEntry.position" />
<div class="whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap" v-html="colorize(logEntry.message)"></div>
</div>
</template>
<script lang="ts" setup>
@@ -24,15 +18,3 @@ defineProps<{
const { markSearch } = useSearchFilter();
const colorize = (value: string) => markSearch(ansiConvertor.toHtml(value));
</script>
<style lang="scss" scoped>
.disable-wrap {
.text {
white-space: nowrap;
}
}
.text {
white-space: pre-wrap;
}
</style>
@@ -1,8 +1,10 @@
<template>
<div class="is-flex-grow-1 has-text-centered my-4">
<div class="is-relative">
<zig-zag class="is-overlay mt-2"></zig-zag>
<span class="text is-relative py-2 px-4">{{ $t("error.logs-skipped", { total: logEntry.totalSkipped }) }}</span>
<div class="my-4 flex-1 text-center">
<div class="relative">
<zig-zag class="absolute inset-0 mt-2"></zig-zag>
<span class="relative whitespace-pre-wrap bg-base px-4 py-2 font-bold">{{
$t("error.logs-skipped", { total: logEntry.totalSkipped })
}}</span>
</div>
</div>
</template>
@@ -13,11 +15,3 @@ defineProps<{
logEntry: SkippedLogsEntry;
}>();
</script>
<style lang="scss" scoped>
.text {
font-weight: bold;
white-space: pre-wrap;
background-color: var(--body-background-color);
}
</style>
+6 -33
View File
@@ -1,13 +1,13 @@
<template>
<div class="has-text-centered is-relative host" @mouseenter="mouseOver = true" @mouseleave="mouseOver = false">
<div class="has-border has-boxshadow is-hidden-mobile">
<div class="relative hover:text-secondary" @mouseenter="mouseOver = true" @mouseleave="mouseOver = false">
<div class="mobile-hidden flex overflow-hidden rounded-sm border border-primary px-px pb-px pt-1">
<stat-sparkline :data="data" @selected-point="onSelectedPoint"></stat-sparkline>
</div>
<div class="has-background-body-color is-top-left">
<span class="has-text-weight-light">{{ label }}</span>
<span class="has-text-weight-bold">
<div class="inline-flex gap-1 rounded bg-base p-px text-xs lg:absolute lg:-left-0.5 lg:-top-2">
<div class="font-light uppercase">{{ label }}</div>
<div class="select-none font-bold">
{{ mouseOver ? selectedPoint?.value ?? selectedPoint?.y ?? statValue : statValue }}
</span>
</div>
</div>
</div>
</template>
@@ -23,30 +23,3 @@ function onSelectedPoint(point: Point<unknown>) {
let mouseOver = $ref(false);
</script>
<style lang="scss" scoped>
.has-border {
border: 1px solid var(--primary-color);
border-radius: 3px;
padding: 1px 1px 0 1px;
display: flex;
overflow: hidden;
padding-top: 0.25em;
}
.has-background-body-color {
background-color: var(--body-background-color);
}
.host:hover span {
color: var(--secondary-color);
}
@media screen and (min-width: 768px) {
.is-top-left {
position: absolute;
top: 0;
left: 0.75em;
}
}
</style>
+3 -19
View File
@@ -1,7 +1,7 @@
<template>
<svg :width="width" :height="height" @mousemove="onMove">
<path :d="path" class="area" />
<line :x1="lineX" y1="0" :x2="lineX" :y2="height" class="line" />
<svg :width="width" :height="height" @mousemove="onMove" class="group">
<path :d="path" class="fill-primary" />
<line :x1="lineX" y1="0" :x2="lineX" :y2="height" class="invisible stroke-secondary stroke-2 group-hover:visible" />
</svg>
</template>
@@ -42,19 +42,3 @@ function onMove(e: MouseEvent) {
selectedPoint(point);
}
</script>
<style scoped>
:deep(.area) {
fill: var(--primary-color);
}
:deep(.line) {
stroke: var(--secondary-color);
stroke-width: 2;
display: none;
}
svg:hover :deep(.line) {
display: unset;
}
</style>
+2 -10
View File
@@ -2,18 +2,10 @@
<svg width="100%" height="8" class="zigzag">
<defs>
<pattern id="zigzag" x="0" y="0" width="30" height="8" patternUnits="userSpaceOnUse">
<line x1="0" y1="0" x2="15" y2="8" class="line" />
<line x1="15" y1="8" x2="30" y2="0" class="line" />
<line x1="0" y1="0" x2="15" y2="8" class="stroke-primary stroke-1" />
<line x1="15" y1="8" x2="30" y2="0" class="stroke-primary stroke-1" />
</pattern>
</defs>
<rect x="0" y="0" width="100%" height="100%" fill="url(#zigzag)"></rect>
</svg>
</template>
<style lang="scss" scoped>
.line {
stroke: var(--primary-color);
stroke-width: 1;
stroke-linecap: round;
}
</style>
@@ -1,237 +1,105 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<LogEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
"<ul data-v-2e92daca=\\"\\" class=\\"events group py-4 medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-2e92daca=\\"\\" class=\\"flex items-start gap-x-2\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
<div data-v-961504e7=\\"\\" class=\\"inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] text-sm\\" size=\\"small\\">
<div class=\\"inline-flex gap-2 whitespace-nowrap text-blue\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"mobile-hidden\\">06/12/2019</time><time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
<div data-v-e625cddd=\\"\\" class=\\"mt-1.5 h-2.5 w-2.5 flex-none rounded-lg flex\\"></div>
<div class=\\"whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render dates with 24 hour style 1`] = `
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
"<ul data-v-2e92daca=\\"\\" class=\\"events group py-4 medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-2e92daca=\\"\\" class=\\"flex items-start gap-x-2\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42</time></div>
<div data-v-961504e7=\\"\\" class=\\"inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] text-sm\\" size=\\"small\\">
<div class=\\"inline-flex gap-2 whitespace-nowrap text-blue\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"mobile-hidden\\">06/12/2019</time><time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
<div data-v-e625cddd=\\"\\" class=\\"mt-1.5 h-2.5 w-2.5 flex-none rounded-lg flex\\"></div>
<div class=\\"whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages 1`] = `
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
"<ul data-v-2e92daca=\\"\\" class=\\"events group py-4 medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-2e92daca=\\"\\" class=\\"flex items-start gap-x-2\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
<div data-v-961504e7=\\"\\" class=\\"inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] text-sm\\" size=\\"small\\">
<div class=\\"inline-flex gap-2 whitespace-nowrap text-blue\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"mobile-hidden\\">06/12/2019</time><time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\">This is a message.</div>
<div data-v-e625cddd=\\"\\" class=\\"mt-1.5 h-2.5 w-2.5 flex-none rounded-lg flex\\"></div>
<div class=\\"whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap\\">This is a message.</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with color 1`] = `
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
"<ul data-v-2e92daca=\\"\\" class=\\"events group py-4 medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-2e92daca=\\"\\" class=\\"flex items-start gap-x-2\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
<div data-v-961504e7=\\"\\" class=\\"inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] text-sm\\" size=\\"small\\">
<div class=\\"inline-flex gap-2 whitespace-nowrap text-blue\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"mobile-hidden\\">06/12/2019</time><time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\"><span style=\\"color:#000\\">black<span style=\\"color:#AAA\\">white</span></span></div>
<div data-v-e625cddd=\\"\\" class=\\"mt-1.5 h-2.5 w-2.5 flex-none rounded-lg flex\\"></div>
<div class=\\"whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap\\"><span style=\\"color:#000\\">black<span style=\\"color:#AAA\\">white</span></span></div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with filter 1`] = `
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"2\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#2\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
"<ul data-v-2e92daca=\\"\\" class=\\"events group py-4 medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"2\\" class=\\"\\"><a data-v-2e92daca=\\"\\" class=\\"jump-context tooltip tooltip-right tooltip-primary\\" data-tip=\\"Jump to Context\\" href=\\"#2\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\">
<path fill=\\"currentColor\\" d=\\"M20 19.59V8l-6-6H4v20l15.57-.02l-4.81-4.81c-.8.52-1.74.83-2.76.83c-2.76 0-5-2.24-5-5s2.24-5 5-5s5 2.24 5 5c0 1.02-.31 1.96-.83 2.75L20 19.59zM9 13c0 1.66 1.34 3 3 3s3-1.34 3-3s-1.34-3-3-3s-3 1.34-3 3z\\"></path>
</svg></a>
<div data-v-2e92daca=\\"\\" class=\\"flex items-start gap-x-2\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
<div data-v-961504e7=\\"\\" class=\\"inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] text-sm\\" size=\\"small\\">
<div class=\\"inline-flex gap-2 whitespace-nowrap text-blue\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"mobile-hidden\\">06/12/2019</time><time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\"><mark>test</mark> bar</div>
<div data-v-e625cddd=\\"\\" class=\\"mt-1.5 h-2.5 w-2.5 flex-none rounded-lg flex\\"></div>
<div class=\\"whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap\\"><mark>test</mark> bar</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with html entities 1`] = `
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
"<ul data-v-2e92daca=\\"\\" class=\\"events group py-4 medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-2e92daca=\\"\\" class=\\"flex items-start gap-x-2\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
<div data-v-961504e7=\\"\\" class=\\"inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] text-sm\\" size=\\"small\\">
<div class=\\"inline-flex gap-2 whitespace-nowrap text-blue\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"mobile-hidden\\">06/12/2019</time><time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
<div data-v-e625cddd=\\"\\" class=\\"mt-1.5 h-2.5 w-2.5 flex-none rounded-lg flex\\"></div>
<div class=\\"whitespace-pre-wrap group-[.disable-wrap]:whitespace-nowrap\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > renders correctly 1`] = `
"<div data-v-1cd63c6e=\\"\\" class=\\"infinte-loader\\">
<div data-v-1cd63c6e=\\"\\" class=\\"spinner\\" style=\\"display: none;\\">
<div data-v-1cd63c6e=\\"\\" class=\\"bounce1\\"></div>
<div data-v-1cd63c6e=\\"\\" class=\\"bounce2\\"></div>
<div data-v-1cd63c6e=\\"\\" class=\\"bounce3\\"></div>
</div>
</div>
<ul data-v-2e92daca=\\"\\" class=\\"events medium\\"></ul>"
"<div class=\\"flex min-h-[1px] justify-center\\"><span class=\\"loading loading-bars loading-md mt-4 text-primary\\" style=\\"display: none;\\"></span></div>
<ul data-v-2e92daca=\\"\\" class=\\"events group py-4 medium\\"></ul>"
`;
exports[`<LogEventSource /> > should parse messages 1`] = `
+12
View File
@@ -0,0 +1,12 @@
<template>
<div class="flex flex-col gap-8 px-4 py-4 md:px-8">
<section>
<links />
</section>
<slot></slot>
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>
+28 -21
View File
@@ -1,25 +1,28 @@
<template>
<div @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" ref="trigger"><slot></slot></div>
<Teleport to="body">
<Transition name="fade">
<div v-show="show && (delayedShow || glopbalShow)" class="content" ref="content">
<slot></slot>
<teleport to="body">
<transition name="fade">
<div
v-show="show && (delayedShow || glopbalShow)"
class="fixed z-50 rounded border border-secondary/50 bg-base-lighter p-4 shadow"
ref="content"
>
<slot name="content"></slot>
</div>
</Transition>
</Teleport>
</transition>
</teleport>
</template>
<script lang="ts" setup>
import { globalShowPopup } from "@/composables/popup";
import { globalShowPopup } from "@/composable/popup";
let glopbalShow = globalShowPopup();
let show = ref(glopbalShow.value);
let delayedShow = refDebounced(show, 1000);
let content: HTMLElement | null = $ref(null);
let trigger: HTMLElement | null = $ref(null);
function onMouseEnter(e: MouseEvent) {
const onMouseEnter = (e: Event) => {
show.value = true;
glopbalShow.value = true;
if (e.target && content && e.target instanceof Element) {
@@ -30,22 +33,26 @@ function onMouseEnter(e: MouseEvent) {
content.style.left = `${x}px`;
content.style.top = `${y}px`;
}
}
};
function onMouseLeave() {
const onMouseLeave = () => {
show.value = false;
glopbalShow.value = false;
}
};
const el = useCurrentElement();
useEventListener(() => el.value?.nextElementSibling, "mouseenter", onMouseEnter);
useEventListener(() => el.value?.nextElementSibling, "mouseleave", onMouseLeave);
</script>
<style scoped>
.content {
position: fixed;
z-index: 9999;
background: var(--scheme-main-ter);
border-radius: 0.5em;
padding: 1em;
box-shadow: 0 0 0.5em rgba(0, 0, 0, 0.5);
border: 1px solid var(--border-color);
<style scoped lang="postcss">
.fade-enter-active,
.fade-leave-active {
@apply transition-opacity;
}
.fade-enter-from,
.fade-leave-to {
@apply opacity-0;
}
</style>
+74
View File
@@ -0,0 +1,74 @@
<template>
<ul class="space-y-4 p-2">
<li v-for="release in releases" v-if="releases?.length">
<div class="flex items-baseline gap-1">
<carbon:warning class="h-4.25 w-4.25 self-center stroke-orange" v-if="release.breaking > 0" />
<a :href="release.htmlUrl" class="link-primary text-lg font-bold" target="_blank" rel="noreferrer noopener">
{{ release.name }}
</a>
<span class="ml-1 text-xs"><distance-time :date="new Date(release.createdAt)" /></span>
<tag class="ml-auto bg-red px-1 py-1 text-xs" v-if="release.tag === latest?.tag">
{{ $t("releases.latest") }}
</tag>
</div>
<div class="text-sm text-base-content/80">
{{ summary(release) }}
</div>
</li>
<li v-else>
<div class="text-sm text-base-content/80">
{{ $t("releases.no_releases") }}
</div>
</li>
</ul>
</template>
<script setup lang="ts">
const { releases, latest } = useReleases();
const { t } = useI18n();
function summary(release: { features: number; bugFixes: number; breaking: number }) {
if (release.features > 0 && release.bugFixes > 0 && release.breaking > 0) {
return t("releases.three_parts", {
first: t("releases.breaking", { count: release.breaking }),
second: t("releases.features", { count: release.features }),
third: t("releases.bugFixes", { count: release.bugFixes }),
});
}
if (release.features > 0 && release.bugFixes > 0) {
return t("releases.two_parts", {
first: t("releases.features", { count: release.features }),
second: t("releases.bugFixes", { count: release.bugFixes }),
});
}
if (release.features > 0 && release.breaking > 0) {
return t("releases.two_parts", {
first: t("releases.features", { count: release.features }),
second: t("releases.breaking", { count: release.breaking }),
});
}
if (release.bugFixes > 0 && release.breaking > 0) {
return t("releases.two_parts", {
first: t("releases.bugFixes", { count: release.bugFixes }),
second: t("releases.breaking", { count: release.breaking }),
});
}
if (release.features > 0) {
return t("releases.features", { count: release.features });
}
if (release.bugFixes > 0) {
return t("releases.bugFixes", { count: release.bugFixes });
}
if (release.breaking > 0) {
return t("releases.breaking", { count: release.breaking });
}
}
</script>
<style scoped></style>
+47 -54
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="fadeout">
<div class="pointer-events-none relative inline-block" ref="root" v-show="!autoHide || show">
<svg width="100" height="100" viewBox="0 0 100 100" :class="{ indeterminate }">
<circle r="44" cx="50" cy="50" class="fill-base-darker stroke-primary" />
</svg>
<div class="absolute inset-0 flex items-center justify-center font-light">
<template v-if="indeterminate">
<div class="text-4xl">&#8734;</div>
</template>
<template v-else-if="!isNaN(scrollProgress)">
<span class="text-4xl">
{{ Math.ceil(scrollProgress * 100) }}
</span>
<span> % </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 as Ref<HTMLElement | Document>, { throttle: 100 });
const show = autoResetRef(false, 2000);
onMounted(() => {
watch(
@@ -46,51 +48,34 @@ 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>
<style scoped lang="scss">
.scroll-progress {
display: inline-block;
position: relative;
pointer-events: none;
<style scoped lang="postcss">
svg {
filter: drop-shadow(0px 1px 1px rgba(0, 0, 0, 0.2));
margin-top: 5px;
&.indeterminate {
animation: 2s linear infinite svg-animation;
svg {
filter: drop-shadow(0px 1px 1px rgba(0, 0, 0, 0.2));
margin-top: 5px;
&.indeterminate {
animation: 2s linear infinite svg-animation;
circle {
animation: 1.4s ease-in-out infinite both circle-animation;
}
}
circle {
fill: var(--scheme-main-ter);
fill-opacity: 0.8;
transition: stroke-dashoffset 250ms ease-out;
transform: rotate(-90deg);
transform-origin: 50% 50%;
stroke: var(--primary-color);
stroke-dashoffset: calc(276.32px - v-bind(scrollProgress) * 276.32px);
stroke-dasharray: 276.32px 276.32px;
stroke-linecap: round;
stroke-width: 3;
will-change: stroke-dashoffset;
animation: 1.4s ease-in-out infinite both circle-animation;
}
}
circle {
fill-opacity: 0.75;
transition: stroke-dashoffset 250ms ease-out;
transform: rotate(-90deg);
transform-origin: 50% 50%;
stroke-dashoffset: calc(276.32px - v-bind(scrollProgress) * 276.32px);
stroke-dasharray: 276.32px 276.32px;
stroke-linecap: round;
stroke-width: 3;
will-change: stroke-dashoffset;
}
}
@keyframes svg-animation {
@@ -119,4 +104,12 @@ watchPostEffect(() => {
transform: rotate(360deg);
}
}
.fadeout-leave-active {
@apply transition-opacity;
}
.fadeout-leave-to {
opacity: 0;
}
</style>
+23 -106
View File
@@ -1,28 +1,31 @@
<template>
<section :class="{ 'is-full-height-scrollable': scrollable }">
<header v-if="$slots.header">
<section :class="{ 'h-screen min-h-0': scrollable }" class="flex flex-col">
<header
v-if="$slots.header"
class="sticky top-[70px] z-[2] border-b border-base-content/10 bg-base py-2 shadow-[1px_1px_2px_0_rgb(0,0,0,0.05)] md:top-0"
>
<slot name="header"></slot>
</header>
<main :data-scrolling="scrollable ? true : undefined">
<div class="is-scrollbar-progress is-hidden-mobile">
<scroll-progress v-show="paused" :indeterminate="loading" :auto-hide="!loading"></scroll-progress>
<main :data-scrolling="scrollable ? true : undefined" class="snap-y overflow-auto">
<div class="invisible mr-28 text-right md:visible" v-show="paused">
<scroll-progress :indeterminate="loading" :auto-hide="!loading" class="z-2 !fixed top-16" />
</div>
<div ref="scrollableContent">
<slot :setLoading="setLoading"></slot>
</div>
<div ref="scrollObserver" class="is-scroll-observer"></div>
<div ref="scrollObserver" class="h-px"></div>
</main>
<div class="is-scrollbar-notification">
<div class="mr-16 text-right">
<transition name="fade">
<button
class="button has-boxshadow"
:class="hasMore ? 'has-more' : ''"
class="btn btn-primary fixed bottom-8 rounded p-3 text-primary-content shadow transition-colors"
:class="hasMore ? 'btn-secondary animate-bounce-fast text-secondary-content' : ''"
@click="scrollToBottom()"
v-show="paused"
>
<mdi:light-chevron-double-down />
<mdi:chevron-double-down />
</button>
</transition>
</div>
@@ -71,106 +74,20 @@ function setLoading(value: boolean) {
loading = value;
}
</script>
<style scoped lang="scss">
section {
display: flex;
flex-direction: column;
<style scoped lang="postcss">
.fade-enter-active,
.fade-leave-active {
@apply transition-opacity;
}
header {
position: sticky;
top: 0;
background: var(--body-background-color);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
z-index: 1;
@media screen and (max-width: 768px) {
top: 70px;
}
}
&.is-full-height-scrollable {
height: 100vh;
min-height: 0;
}
main {
flex: 1;
overflow: auto;
scroll-snap-type: y proximity;
}
.is-scrollbar-progress {
text-align: right;
margin-right: 110px;
.scroll-progress {
position: fixed;
top: 60px;
z-index: 2;
}
}
.is-scroll-observer {
height: 1px;
}
.is-scrollbar-notification {
text-align: right;
margin-right: 65px;
button {
position: fixed;
bottom: 30px;
background-color: var(--primary-color);
transition: background-color 0.24s ease-out;
border: none !important;
color: #eee;
&.has-more {
background-color: var(--secondary-color);
animation-name: bounce;
animation-duration: 1000ms;
animation-fill-mode: both;
color: #222;
}
}
}
@keyframes bounce {
0%,
20%,
50%,
80%,
100% {
transform: translateY(0);
}
40% {
transform: translateY(-30px);
}
60% {
transform: translateY(-15px);
}
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.15s ease-in;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.fade-enter-from,
.fade-leave-to {
@apply opacity-0;
}
</style>
<style>
@media screen and (max-width: 768px) {
.splitpanes__pane {
overflow: unset !important;
}
.splitpanes__pane {
overflow: unset !important;
}
</style>
+17 -61
View File
@@ -1,23 +1,22 @@
<template>
<div class="search columns is-gapless is-vcentered" v-show="showSearch" v-if="search">
<div class="column">
<p class="control has-icons-left">
<input
class="input"
type="text"
placeholder="Find / RegEx"
ref="input"
v-model="searchFilter"
@keyup.esc="resetSearch()"
/>
<span class="icon is-left">
<mdi:light-magnify />
</span>
</p>
</div>
<div class="column is-1 has-text-centered">
<button class="delete is-medium" @click="resetSearch()"></button>
<div
class="fixed -right-px -top-px z-10 flex w-96 items-center gap-4 rounded-bl border border-secondary/20 bg-base-darker p-4 shadow"
v-show="showSearch"
v-if="search"
>
<div class="input input-primary flex h-auto items-center">
<mdi:magnify />
<input
class="input input-ghost flex-1"
type="text"
placeholder="Find / RegEx"
ref="input"
v-model="searchFilter"
@keyup.esc="resetSearch()"
/>
</div>
<a class="btn btn-circle btn-xs" @click="resetSearch()"> <mdi:close /></a>
</div>
</template>
@@ -35,46 +34,3 @@ onKeyStroke("f", (e) => {
onUnmounted(() => resetSearch());
</script>
<style lang="scss" scoped>
.search {
width: 350px;
position: fixed;
padding: 10px;
background: var(--scheme-main-ter);
top: 0;
right: 0;
border-radius: 0 0 0 5px;
z-index: 10;
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.12),
0 1px 2px rgba(0, 0, 0, 0.24);
button.delete {
margin-left: 1em;
background-color: var(--scheme-main-ter);
opacity: 0.6;
&:after,
&:before {
background-color: var(--text-color);
}
&:hover {
opacity: 1;
}
}
.icon {
padding: 10px 3px;
}
.input {
color: var(--body-color);
&::placeholder {
color: var(--border-color);
}
}
}
</style>
+105 -64
View File
@@ -1,67 +1,71 @@
<template>
<div v-if="ready">
<nav class="breadcrumb menu-label" aria-label="breadcrumbs">
<ul v-if="sessionHost">
<li>
<a href="#" @click.prevent="setHost(null)">{{ hosts[sessionHost].name }}</a>
</li>
<li class="is-active">
<a href="#" aria-current="page">{{ $t("label.containers") }}</a>
<div v-if="ready" data-testid="side-menu">
<div class="breadcrumbs">
<ul>
<li><a @click.prevent="setHost(null)" class="link-primary">Hosts</a></li>
<li v-if="sessionHost && hosts[sessionHost]" class="cursor-default">
{{ hosts[sessionHost].name }}
</li>
</ul>
<ul v-else>
<li>Hosts</li>
</ul>
</nav>
</div>
<transition :name="sessionHost ? 'slide-left' : 'slide-right'" mode="out-in">
<ul class="menu-list" v-if="!sessionHost">
<ul class="menu p-0" v-if="!sessionHost">
<li v-for="host in config.hosts">
<a @click.prevent="setHost(host.id)">{{ host.name }}</a>
<a @click.prevent="setHost(host.id)">
<ph:computer-tower />
{{ host.name }}
</a>
</li>
</ul>
<ul class="menu-list" v-else>
<li v-for="item in sortedContainers" :key="item.id" :class="item.state">
<popup>
<transition-group tag="ul" name="list" class="containers menu p-0 [&_li.menu-title]:px-0" v-else>
<li
v-for="item in menuItems"
:key="item.id"
:class="isLabel(item) ? 'menu-title' : item.state"
:data-testid="item.id"
>
<template v-if="isLabel(item)">
{{ item.label }}
</template>
<popup v-else>
<router-link
:to="{ name: 'container-id', params: { id: item.id } }"
active-class="is-active"
active-class="active-primary"
:title="item.name"
>
<div class="container is-flex is-align-items-center">
<div class="is-flex-grow-1 is-ellipsis">
<span>{{ item.name }}</span
><span class="has-text-weight-light has-light-opacity" v-if="item.isSwarm">{{ item.swarmId }}</span>
</div>
<div class="is-flex-shrink-1 is-flex icons">
<div
class="icon is-small pin"
@click.stop.prevent="store.appendActiveContainer(item)"
v-show="!activeContainersById[item.id]"
:title="$t('tooltip.pin-column')"
>
<cil:columns />
</div>
<container-health :health="item.health"></container-health>
</div>
<div class="truncate">
{{ item.name }}<span class="font-light opacity-70" v-if="item.isSwarm">{{ item.swarmId }}</span>
</div>
<span
class="pin"
@click.stop.prevent="store.appendActiveContainer(item)"
v-show="!activeContainersById[item.id]"
:title="$t('tooltip.pin-column')"
>
<cil:columns />
</span>
<container-health :health="item.health"></container-health>
</router-link>
<template #content>
<container-popup :container="item"></container-popup>
</template>
</popup>
</li>
</ul>
</transition-group>
</transition>
</div>
<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>
<div role="status" class="flex animate-pulse flex-col gap-4" v-else>
<div class="h-3 w-full rounded-full bg-base-content/50 opacity-50" v-for="_ in 9"></div>
<span class="sr-only">Loading...</span>
</div>
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
import { sessionHost } from "@/composables/storage";
import { sessionHost } from "@/composable/storage";
const { t } = useI18n();
const store = useContainerStore();
@@ -71,6 +75,7 @@ function setHost(host: string | null) {
sessionHost.value = host;
}
const debouncedIds = debouncedRef(pinnedContainers, 200);
const sortedContainers = computed(() =>
visibleContainers.value
.filter((c) => c.host === sessionHost.value)
@@ -85,6 +90,36 @@ const sortedContainers = computed(() =>
}),
);
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: "containers", 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) => {
@@ -105,39 +140,29 @@ const activeContainersById = computed(() =>
),
);
</script>
<style scoped lang="scss">
.has-light-opacity {
opacity: 0.5;
<style scoped lang="postcss">
.menu {
@apply text-[0.95rem];
}
.loading {
opacity: 0.5;
}
li.exited a,
li.dead a {
color: #777;
}
.icons {
column-gap: 0.35em;
align-items: baseline;
}
a {
.containers a {
@apply auto-cols-[auto_max-content];
.pin {
display: none;
&:hover {
color: var(--secondary-color);
@apply text-secondary;
}
}
&:hover {
.pin {
display: block;
display: inline-block;
}
}
}
li.exited {
@apply opacity-50;
}
.slide-left-enter-active,
.slide-left-leave-active,
@@ -148,21 +173,37 @@ a {
.slide-left-enter-from {
opacity: 0;
transform: translateX(100%);
transform: translateX(20px);
}
.slide-right-enter-from {
opacity: 0;
transform: translateX(-100%);
transform: translateX(-20px);
}
.slide-left-leave-to {
opacity: 0;
transform: translateX(-100%);
transform: translateX(-20px);
}
.slide-right-leave-to {
opacity: 0;
transform: translateX(100%);
transform: translateX(20px);
}
.list-move,
.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>
+34 -61
View File
@@ -1,48 +1,40 @@
<template>
<aside>
<div class="columns is-marginless">
<div class="column is-paddingless">
<h1>
<router-link :to="{ name: 'index' }">
<svg class="logo">
<use href="#logo"></use>
</svg>
</router-link>
<aside class="fixed h-screen w-[inherit] overflow-auto p-3" data-testid="navigation">
<h1>
<router-link :to="{ name: 'index' }">
<svg class="h-14 w-28 fill-secondary">
<use href="#logo"></use>
</svg>
</router-link>
<small class="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>
<small class="mb-4 block text-xs font-light" v-if="hostname">
{{ hostname }}
</small>
</h1>
<div class="flex gap-4">
<router-link
:to="{ name: 'settings' }"
:aria-label="$t('title.settings')"
data-testid="settings"
class="btn btn-circle btn-sm"
>
<mdi:cog />
</router-link>
<a :href="`${base}/logout`" :title="$t('button.logout')" v-if="secured" class="btn btn-circle btn-sm">
<mdi:logout />
</a>
</div>
<a
class="input input-sm mt-4 inline-flex cursor-pointer items-center gap-2 font-light hover:border-primary"
@click="$emit('search')"
:title="$t('tooltip.search')"
>
<mdi:magnify />
Search
<key-shortcut char="k"></key-shortcut>
</a>
<side-menu class="mt-4"></side-menu>
</aside>
</template>
@@ -50,22 +42,3 @@
<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>
+3 -3
View File
@@ -1,6 +1,6 @@
<template>
<div>
<time :datetime="date.toISOString()" class="is-hidden-mobile">{{ dateStr }}</time>
<div class="inline-flex gap-2">
<time :datetime="date.toISOString()" class="mobile-hidden">{{ dateStr }}</time>
<time :datetime="date.toISOString()">{{ timeStr }}</time>
</div>
</template>
@@ -14,7 +14,7 @@ const dateFormatter = new Intl.DateTimeFormat(undefined, { day: "2-digit", month
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 }),
new Intl.DateTimeFormat(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: use12Hour }),
);
const dateStr = $computed(() => dateFormatter.format(props.date));
@@ -17,6 +17,9 @@ const {
}>();
const text = ref<string>();
watch($$(date), updateFromNow, { immediate: true });
function updateFromNow() {
const fn = strict ? formatDistanceToNowStrict : formatDistanceToNow;
text.value = fn(date, {
+25
View File
@@ -0,0 +1,25 @@
<template>
<div class="dropdown">
<label tabindex="0" class="btn btn-circle btn-sm" @mousedown="checkAndCloseDropDown($event)" @blur="closed()">
<slot name="trigger"></slot>
</label>
<div
tabindex="0"
class="min-w-52 dropdown-content rounded-box z-50 mt-1 border border-base-content/20 bg-base p-2 shadow"
>
<slot name="content"></slot>
</div>
</div>
</template>
<script setup lang="ts">
const closed = defineEmit();
function checkAndCloseDropDown(e: MouseEvent) {
const target = e.currentTarget as HTMLElement;
if (target?.matches(":focus")) {
setTimeout(() => target.blur(), 0);
}
}
</script>
<style scoped></style>
+44
View File
@@ -0,0 +1,44 @@
<template>
<details class="dropdown" ref="details" v-on-click-outside="close">
<summary class="btn btn-primary flex-nowrap" v-bind="$attrs">
<slot name="trigger"> {{ values[modelValue] ?? defaultLabel }} <carbon:caret-down /></slot>
</summary>
<ul class="menu dropdown-content rounded-box z-50 mt-1 w-52 border border-base-content/20 bg-base p-2 shadow">
<slot>
<li v-for="item in options">
<a @click="modelValue = item.value">
<mdi:check class="w-4" v-if="modelValue == item.value" />
<div v-else class="w-4"></div>
{{ item.label }}
</a>
</li>
</slot>
</ul>
</details>
</template>
<script lang="ts" setup>
import { vOnClickOutside } from "@vueuse/components";
type DropdownItem = {
label: string;
value: string;
};
const { options = [], defaultLabel = "" } = defineProps<{ options?: DropdownItem[]; defaultLabel?: string }>();
const { modelValue } = defineModels<{
modelValue: string;
}>();
const values = computed(() =>
options.reduce(
(acc, curr) => {
acc[curr.value] = curr.label;
return acc;
},
{} as Record<string, string>,
),
);
const details = ref<HTMLElement | null>(null);
const close = () => details.value?.removeAttribute("open");
watch(modelValue, () => close());
</script>
+19
View File
@@ -0,0 +1,19 @@
<template>
<div class="inline-flex items-center">
<template v-if="modifiers.includes('shift')">
<carbon:mac-shift />
</template>
<template v-if="modifiers.includes('meta')">
<ph:command v-if="isMac" class="h-4 w-4" />
<ph:control-bold v-else class="h-4 w-4" />
</template>
<kbd class="uppercase">{{ char }}</kbd>
</div>
</template>
<script lang="ts">
const isMac = /(Mac|iPhone|iPod|iPad)/i.test(navigator.userAgent);
</script>
<script lang="ts" setup>
const { modifiers = ["meta"], char } = defineProps<{ char: string; modifiers?: ("^" | "meta" | "shift")[] }>();
</script>
+65 -128
View File
@@ -1,101 +1,71 @@
<template>
<aside>
<div class="columns is-marginless is-gapless is-mobile is-vcentered">
<div class="column is-narrow">
<router-link :to="{ name: 'index' }">
<svg class="logo">
<use href="#logo"></use>
</svg>
</router-link>
</div>
<nav class="fixed top-0 z-10 w-full border-b border-base-content/20 bg-base p-2" data-testid="navigation">
<div class="flex items-center">
<router-link :to="{ name: 'index' }">
<svg class="h-14 w-28 fill-secondary">
<use href="#logo"></use>
</svg>
</router-link>
<div class="column is-narrow push-right">
<a
role="button"
class="navbar-burger burger is-hidden-tablet is-pulled-right"
@click="showNav = !showNav"
:class="{ 'is-active': showNav }"
>
<span></span> <span></span> <span></span>
<div class="ml-auto flex items-center gap-2">
<a class="btn btn-circle flex" @click="$emit('search')" :title="$t('tooltip.search')">
<mdi:magnify class="h-5 w-5" />
</a>
<label class="btn btn-circle swap swap-rotate" data-testid="hamburger">
<input type="checkbox" v-model="show" />
<mdi:close class="swap-on" />
<mdi:hamburger-menu class="swap-off" />
</label>
</div>
</div>
<div class="menu-label level is-mobile is-hidden-mobile" :class="{ 'is-active': showNav }">
<div v-if="config.hosts.length > 1">
<o-dropdown v-model="sessionHost" aria-role="list">
<template #trigger>
<o-button variant="primary" type="button" size="small">
<span>{{ sessionHost ? hosts[sessionHost].name : "" }}</span>
<span class="icon">
<carbon:caret-down />
</span>
</o-button>
</template>
<o-dropdown-item :value="value.id" aria-role="listitem" v-for="value in config.hosts" :key="value">
<span>{{ value.name }}</span>
</o-dropdown-item>
</o-dropdown>
</div>
<div class="level-item has-text-centered">
<div>
<button class="button is-small is-rounded" @click="$emit('search')" :title="$t('tooltip.search')">
<span class="icon">
<mdi:light-magnify />
</span>
</button>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<router-link :to="{ name: 'settings' }" active-class="is-active" class="button is-small is-rounded">
<span class="icon">
<mdi:light-cog />
</span>
<transition name="fade">
<div v-show="show">
<div class="mt-4 flex items-center justify-center gap-2">
<dropdown-menu
v-model="sessionHost"
:options="hosts"
defaultLabel="Hosts"
class="btn-sm"
v-if="config.hosts.length > 1"
/>
<router-link :to="{ name: 'settings' }" class="btn btn-outline btn-sm">
<mdi:cog /> {{ $t("button.settings") }}
</router-link>
</div>
</div>
<div class="level-item has-text-centered" v-if="secured">
<div>
<a class="button is-small is-rounded" :href="`${base}/logout`" :title="$t('button.logout')">
<span class="icon">
<mdi:light-logout />
</span>
<a class="btn btn-outline btn-sm" :href="`${base}/logout`" :title="$t('button.logout')" v-if="secured">
<mdi:logout /> {{ $t("button.logout") }}
</a>
</div>
</div>
</div>
<p class="menu-label is-hidden-mobile" :class="{ 'is-active': showNav }">{{ $t("label.containers") }}</p>
<ul class="menu-list is-hidden-mobile" :class="{ 'is-active': showNav }">
<li v-for="item in sortedContainers" :key="item.id">
<router-link
:to="{ name: 'container-id', params: { id: item.id } }"
active-class="is-active"
:title="item.name"
>
<div class="is-ellipsis">
{{ item.name }}
</div>
</router-link>
</li>
</ul>
</aside>
<ul class="menu">
<li class="menu-title">{{ $t("label.containers") }}</li>
<li v-for="item in sortedContainers" :key="item.id">
<router-link
:to="{ name: 'container-id', params: { id: item.id } }"
active-class="active-primary"
class="truncate"
:title="item.name"
>
{{ item.name }}
</router-link>
</li>
</ul>
</div>
</transition>
</nav>
</template>
<script lang="ts" setup>
const { base, secured } = config;
import { sessionHost } from "@/composables/storage";
import { sessionHost } from "@/composable/storage";
const store = useContainerStore();
const route = useRoute();
const { visibleContainers } = storeToRefs(store);
let showNav = $ref(false);
const show = ref(false);
watch(route, () => {
showNav = false;
show.value = false;
});
const sortedContainers = computed(() =>
@@ -112,59 +82,26 @@ const sortedContainers = computed(() =>
}),
);
const hosts = computed(() =>
config.hosts.reduce(
(acc, item) => {
acc[item.id] = item;
return acc;
},
{} as Record<string, { name: string; id: string }>,
),
);
const hosts = computed(() => config.hosts.map(({ id, name }) => ({ value: id, label: name })));
</script>
<style scoped lang="scss">
aside {
padding: 1em;
position: fixed;
left: 0;
right: 0;
background: var(--scheme-main-ter);
z-index: 10;
max-height: 100vh;
overflow: auto;
<style scoped lang="postcss">
.fade-enter-active,
.fade-leave-active {
@apply transition-opacity;
}
.level.is-hidden-mobile.is-active {
display: flex !important;
}
.fade-enter-active .menu,
.fade-leave-active .menu {
@apply transition-transform;
}
.menu-label {
margin-top: 1em;
}
.fade-enter-from,
.fade-leave-to {
@apply opacity-0;
}
.title {
text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.2);
}
.burger {
color: var(--body-color);
}
.is-hidden-mobile.is-active {
display: block !important;
}
.navbar-burger {
height: 2.35rem;
}
.logo {
width: 82px;
height: 36px;
fill: var(--logo-color);
}
.column.push-right {
margin-left: auto;
}
.fade-enter-from .menu,
.fade-leave-to .menu {
@apply -translate-y-2;
}
</style>
+4 -8
View File
@@ -1,5 +1,5 @@
<template>
<div class="tag" :size="size">
<div class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] text-sm" :size="size">
<slot></slot>
</div>
</template>
@@ -8,12 +8,8 @@
const { size = undefined } = defineProps<{ size?: "small" | undefined }>();
</script>
<style scoped lang="scss">
.tag {
background-color: var(--scheme-main-ter);
border: 1px solid var(--border-color);
&[size="small"] {
font-size: 0.61rem;
}
<style scoped lang="postcss">
[size="small"] {
@apply text-xs;
}
</style>
+39
View File
@@ -0,0 +1,39 @@
<template>
<button class="btn relative overflow-hidden" @click="cancel()">
<div class="absolute inset-0 origin-left bg-white/30" ref="progress"></div>
<div class="z-10">
<slot></slot>
</div>
</button>
</template>
<script lang="ts" setup>
const progress = ref<HTMLElement>();
const finished = defineEmit();
const cancelled = defineEmit();
let animation: Animation | undefined;
onMounted(async () => {
animation = progress.value?.animate([{ transform: "scaleX(0)" }, { transform: "scaleX(1)" }], {
duration: 4000,
easing: "linear",
fill: "forwards",
});
try {
await animation?.finished;
finished();
} catch (e) {
progress.value?.animate([{ transform: "scaleX(1)" }, { transform: "scaleX(0)" }], {
duration: 0,
fill: "forwards",
});
cancelled();
}
});
const cancel = () => {
animation?.cancel();
};
</script>
<style scoped lang="postcss"></style>
+12
View File
@@ -0,0 +1,12 @@
<template>
<label class="label inline-flex cursor-pointer gap-4 font-normal">
<input type="checkbox" class="toggle toggle-primary" v-model="modelValue" />
<slot />
</label>
</template>
<script lang="ts" setup>
const { modelValue } = defineModels<{
modelValue: boolean;
}>();
</script>
+23
View File
@@ -0,0 +1,23 @@
import { Container } from "@/models/Container";
type ContainerContext = {
container: Ref<Container>;
streamConfig: { stdout: boolean; stderr: boolean };
};
export const containerContext = Symbol("containerContext") as InjectionKey<ContainerContext>;
export const provideContainerContext = (container: Ref<Container>) => {
provide(containerContext, {
container,
streamConfig: reactive({ stdout: true, stderr: true }),
});
};
export const useContainerContext = () => {
const context = inject(containerContext);
if (!context) {
throw new Error("No container context provided");
}
return context;
};
@@ -1,4 +1,4 @@
import { type ComputedRef, type Ref } from "vue";
import { type Ref } from "vue";
import { encodeXML } from "entities";
import debounce from "lodash.debounce";
import {
@@ -9,7 +9,6 @@ import {
DockerEventLogEntry,
SkippedLogsEntry,
} from "@/models/LogEntry";
import { Container } from "@/models/Container";
function parseMessage(data: string): LogEntry<string | JSONObject> {
const e = JSON.parse(data, (key, value) => {
@@ -21,15 +20,12 @@ function parseMessage(data: string): LogEntry<string | JSONObject> {
return asLogEntry(e);
}
type LogStreamConfig = {
stdout: boolean;
stderr: boolean;
};
export function useLogStream(container: ComputedRef<Container>, streamConfig: LogStreamConfig) {
export function useLogStream() {
const { container, streamConfig } = useContainerContext();
let messages: LogEntry<string | JSONObject>[] = $ref([]);
let buffer: LogEntry<string | JSONObject>[] = $ref([]);
const scrollingPaused = $ref(inject("scrollingPaused") as Ref<boolean>);
let containerId = container.value.id;
function flushNow() {
if (messages.length > config.maxLogs) {
@@ -57,50 +53,54 @@ export function useLogStream(container: ComputedRef<Container>, streamConfig: Lo
}
const flushBuffer = debounce(flushNow, 250, { maxWait: 1000 });
let es: EventSource | null = null;
let lastEventId = "";
function close() {
if (es) {
es.close();
console.debug(`EventSource closed for ${containerId}`);
es = null;
}
}
function clearMessages() {
flushBuffer.cancel();
messages = [];
buffer = [];
console.debug(`Clearing messages for ${containerId}`);
}
function connect({ clear } = { clear: true }) {
es?.close();
close();
if (clear) {
flushBuffer.cancel();
messages = [];
buffer = [];
lastEventId = "";
clearMessages();
}
const params = {
lastEventId,
} as { lastEventId: string; stdout?: string; stderr?: string };
const params = Object.entries(streamConfig)
.filter(([, value]) => value)
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), {});
if (streamConfig.stdout) {
params.stdout = "1";
}
if (streamConfig.stderr) {
params.stderr = "1";
}
containerId = container.value.id;
console.debug(`Connecting to ${containerId} with params`, params);
es = new EventSource(
`${config.base}/api/logs/stream/${container.value.host}/${container.value.id}?${new URLSearchParams(
params,
).toString()}`,
withBase(`/api/logs/stream/${container.value.host}/${containerId}?${new URLSearchParams(params).toString()}`),
);
es.addEventListener("container-stopped", () => {
es?.close();
es = null;
close();
buffer.push(new DockerEventLogEntry("Container stopped", new Date(), "container-stopped"));
flushBuffer();
flushBuffer.flush();
});
es.addEventListener("error", (e) => console.error("EventSource failed: " + JSON.stringify(e)));
es.onmessage = (e) => {
lastEventId = e.lastEventId;
if (e.data) {
buffer.push(parseMessage(e.data));
flushBuffer();
}
};
es.onerror = () => clearMessages();
}
async function loadOlderLogs({ beforeLoading, afterLoading } = { beforeLoading: () => {}, afterLoading: () => {} }) {
@@ -112,23 +112,13 @@ export function useLogStream(container: ComputedRef<Container>, streamConfig: Lo
const delta = to.getTime() - last.getTime();
const from = new Date(to.getTime() + delta);
const params = {
from: from.toISOString(),
to: to.toISOString(),
} as { from: string; to: string; stdout?: string; stderr?: string };
if (streamConfig.stdout) {
params.stdout = "1";
}
if (streamConfig.stderr) {
params.stderr = "1";
}
const params = Object.entries(streamConfig)
.filter(([, value]) => value)
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), { from: from.toISOString(), to: to.toISOString() });
const logs = await (
await fetch(
`${config.base}/api/logs/${container.value.host}/${container.value.id}?${new URLSearchParams(
params,
).toString()}`,
withBase(`/api/logs/${container.value.host}/${containerId}?${new URLSearchParams(params).toString()}`),
)
).text();
if (logs) {
@@ -152,11 +142,7 @@ export function useLogStream(container: ComputedRef<Container>, streamConfig: Lo
},
);
onUnmounted(() => {
if (es) {
es.close();
}
});
onUnmounted(() => close());
watch(
() => container.value.id,
+15
View File
@@ -0,0 +1,15 @@
import { Container } from "@/models/Container";
const DOZZLE_HOST = "DOZZLE_HOST";
export const sessionHost = useSessionStorage<string | null>(DOZZLE_HOST, null);
if (config.hosts.length === 1 && !sessionHost.value) {
sessionHost.value = config.hosts[0].id;
}
export function persistentVisibleKeys(container: Ref<Container>) {
return computed(() => useStorage(container.value.storageKey, []));
}
const DOZZLE_PINNED_CONTAINERS = "DOZZLE_PINNED_CONTAINERS";
export const pinnedContainers = useStorage(DOZZLE_PINNED_CONTAINERS, new Set<string>());
@@ -1,6 +1,6 @@
const { hostname } = config;
let subtitle = $ref("");
const title = $computed(() => `${subtitle} - Dozzle` + (hostname ? ` @ ${hostname}` : ""));
const title = $computed(() => (subtitle ? `${subtitle} - ` : "") + "Dozzle" + (hostname ? ` @ ${hostname}` : ""));
useTitle($$(title));
+45
View File
@@ -0,0 +1,45 @@
type Toast = {
id: string;
createdAt: Date;
title?: string;
message: string;
type: "success" | "error" | "warning" | "info";
};
type ToastOptions = {
expire?: number;
once?: boolean;
};
const toasts = ref<Toast[]>([]);
const showToast = (
toast: Omit<Toast, "id" | "createdAt"> & { id?: string },
{ expire = -1, once = false }: ToastOptions = { expire: -1, once: false },
) => {
if (once && toasts.value.some((t) => t.id === toast.id)) {
return;
}
toasts.value.push({
id: Date.now().toString(),
createdAt: new Date(),
...toast,
});
if (expire > 0) {
setTimeout(() => {
removeToast(toasts.value[0].id);
}, expire);
}
};
const removeToast = (id: Toast["id"]) => {
toasts.value = toasts.value.filter((toast) => toast.id !== id);
};
export const useToast = () => {
return {
toasts,
showToast,
removeToast,
};
};
-99
View File
@@ -1,99 +0,0 @@
const DOZZLE_SETTINGS_KEY = "DOZZLE_SETTINGS";
export const DEFAULT_SETTINGS: {
search: boolean;
size: "small" | "medium" | "large";
menuWidth: number;
smallerScrollbars: boolean;
showTimestamp: boolean;
showStd: boolean;
showAllContainers: boolean;
lightTheme: "auto" | "dark" | "light";
hourStyle: "auto" | "24" | "12";
softWrap: boolean;
collapseNav: boolean;
} = {
search: true,
size: "medium",
menuWidth: 15,
smallerScrollbars: false,
showTimestamp: true,
showStd: false,
showAllContainers: false,
lightTheme: "auto",
hourStyle: "auto",
softWrap: true,
collapseNav: false,
};
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 showStd = computed({
get: () => settings.value.showStd,
set: (value) => (settings.value.showStd = 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 {
collapseNav,
softWrap,
hourStyle,
lightTheme,
showAllContainers,
showTimestamp,
showStd,
smallerScrollbars,
menuWidth,
size,
search,
settings,
};
-13
View File
@@ -1,13 +0,0 @@
import { Container } from "@/models/Container";
const sessionHost = useSessionStorage<string | null>("host", null);
if (config.hosts.length === 1 && !sessionHost.value) {
sessionHost.value = config.hosts[0].id;
}
function persistentVisibleKeys(container: ComputedRef<Container>) {
return computed(() => useStorage(stripVersion(container.value.image) + ":" + container.value.command, []));
}
export { sessionHost, persistentVisibleKeys };
+54 -66
View File
@@ -1,5 +1,5 @@
<template>
<main v-if="!authorizationNeeded">
<div v-if="!authorizationNeeded">
<mobile-menu v-if="isMobile" @search="showFuzzySearch"></mobile-menu>
<splitpanes @resized="onResized($event)">
<pane min-size="10" :size="menuWidth" v-if="!isMobile && !collapseNav">
@@ -7,7 +7,7 @@
</pane>
<pane min-size="10">
<splitpanes>
<pane class="has-min-height router-view">
<pane class="router-view min-h-screen">
<router-view></router-view>
</pane>
<template v-if="!isMobile">
@@ -24,58 +24,77 @@
</splitpanes>
</pane>
</splitpanes>
<button
@click="collapse"
class="button is-small is-rounded"
:class="{ collapsed: collapseNav }"
id="hide-nav"
<label
class="btn btn-circle swap btn-neutral swap-rotate fixed -left-12 bottom-4 w-16 transition-all hover:-left-4"
:class="{ '!-left-6': collapseNav }"
v-if="!isMobile"
>
<span class="icon ml-2" v-if="collapseNav">
<mdi:light-chevron-right />
</span>
<span class="icon" v-else>
<mdi:light-chevron-left />
</span>
</button>
</main>
<input type="checkbox" v-model="collapseNav" />
<mdi:chevron-right class="swap-on" />
<mdi:chevron-left class="swap-off" />
</label>
</div>
<dialog ref="modal" class="modal items-start bg-white/20 backdrop:backdrop-blur-sm" @close="open = false">
<div class="modal-box max-w-2xl bg-transparent pt-20 shadow-none">
<FuzzySearchModal @close="open = false" v-if="open" />
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<div class="toast toast-end whitespace-normal">
<div
class="alert max-w-xl"
v-for="toast in toasts"
:key="toast.id"
:class="{ 'alert-error': toast.type === 'error', 'alert-info': toast.type === 'info' }"
>
<carbon:information class="h-6 w-6 shrink-0 stroke-current" v-if="toast.type === 'info'" />
<carbon:warning class="h-6 w-6 shrink-0 stroke-current" v-else-if="toast.type === 'error'" />
<div>
<h3 class="text-lg font-bold" v-if="toast.title">{{ toast.title }}</h3>
{{ toast.message }}
</div>
<div>
<button class="btn btn-circle btn-xs" @click="removeToast(toast.id)"><mdi:close /></button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
// @ts-ignore - splitpanes types are not available
import { Splitpanes, Pane } from "splitpanes";
import { useProgrammatic } from "@oruga-ui/oruga-next";
import FuzzySearchModal from "@/components/FuzzySearchModal.vue";
const { oruga } = useProgrammatic();
import { collapseNav } from "@/stores/settings";
const { authorizationNeeded } = config;
const containerStore = useContainerStore();
const { activeContainers, visibleContainers } = storeToRefs(containerStore);
const { activeContainers } = storeToRefs(containerStore);
watchEffect(() => {
setTitle(`${visibleContainers.value.length} containers`);
const { toasts, removeToast } = useToast();
const modal = ref<HTMLDialogElement>();
const open = ref(false);
watch(open, () => {
if (open.value) {
modal.value?.showModal();
} else {
modal.value?.close();
}
});
onKeyStroke("k", (e) => {
if (e.ctrlKey || e.metaKey) {
if ((e.ctrlKey || e.metaKey) && !e.shiftKey) {
showFuzzySearch();
e.preventDefault();
}
});
function showFuzzySearch() {
oruga.modal.open({
// parent: this,
component: FuzzySearchModal,
animation: "false",
width: 600,
active: true,
});
}
function collapse() {
collapseNav.value = !collapseNav.value;
open.value = true;
}
function onResized(e: any) {
if (e.length == 2) {
menuWidth.value = e[0].size;
@@ -83,14 +102,9 @@ function onResized(e: any) {
}
</script>
<style scoped lang="scss">
<style scoped lang="postcss">
:deep(.splitpanes--vertical > .splitpanes__splitter) {
min-width: 3px;
background: var(--border-color);
&:hover {
background: var(--border-hover-color);
}
@apply min-w-[3px] bg-base-lighter hover:bg-secondary;
}
@media screen and (max-width: 768px) {
@@ -98,30 +112,4 @@ function onResized(e: any) {
padding-top: 75px;
}
}
.button.has-no-border {
border-color: transparent !important;
}
.has-min-height {
min-height: 100vh;
}
#hide-nav {
position: fixed;
left: 10px;
bottom: 10px;
&.collapsed {
left: -40px;
width: 60px;
padding-left: 40px;
color: var(--text-strong-color);
background: var(--scheme-main);
&:hover {
left: -25px;
}
}
}
</style>
+5 -7
View File
@@ -1,9 +1,7 @@
<template>
<main>
<router-view></router-view>
</main>
<div class="hero min-h-screen">
<div class="hero-content">
<router-view></router-view>
</div>
</div>
</template>
<script lang="ts" setup></script>
<style scoped lang="scss"></style>
+123
View File
@@ -0,0 +1,123 @@
@import "splitpanes/dist/splitpanes.css";
@define-mixin light {
--base-lighter-color: 100% 0 0;
--base-color: 97% 0 0;
--base-darker-color: 90% 0 0;
--base-content-color: 33% 0 0;
--primary-color: 76.96% 0.143 176.65;
--secondary-color: 76.04% 0.168 64.07;
}
@define-mixin dark {
--base-lighter-color: 25% 0 0;
--base-color: 18% 0 0;
--base-darker-color: 11% 0 0;
--base-content-color: 89.23% 0 0;
--primary-color: 76.96% 0.143 176.65;
--secondary-color: 91.36% 0.11098 90.1243;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--p: var(--primary-color);
--pc: 89.23% 0;
--s: var(--secondary-color);
--sc: 14.57% 0 0;
--b1: var(--base-lighter-color);
--b2: var(--base-color);
--b3: var(--base-darker-color);
--bc: var(--base-content-color);
--in: 65% 0.171 249.5;
--inc: 99% 0.171 249.5;
--er: 64% 0.218 28.85;
--erc: 99% 0.218 28.85;
--n: var(--base-lighter-color);
}
html[data-theme="dark"] {
@mixin dark;
}
@media (prefers-color-scheme: dark) {
html {
@mixin dark;
}
}
html[data-theme="light"] {
@mixin light;
}
@media (prefers-color-scheme: light) {
html {
@mixin light;
}
}
@media screen and (max-device-width: 480px) {
body {
-webkit-text-size-adjust: 100%;
}
}
h1 {
@apply text-3xl;
}
h2 {
@apply text-2xl;
}
h3 {
@apply text-lg;
}
mark {
@apply inline-block rounded-sm bg-secondary;
animation: pops 200ms ease-out;
}
@keyframes pops {
0% {
transform: scale(1.5);
}
100% {
transform: scale(1.05);
}
}
}
@layer components {
.input {
@apply !outline-none;
&.input-ghost {
@apply focus:border-transparent;
}
}
.btn {
@apply font-normal normal-case;
}
.btn:not(.btn-primary):not(.btn-secondary) {
@apply hover:bg-base-lighter;
}
}
@layer utilities {
@media (max-width: 768px) {
.mobile-hidden {
display: none;
}
}
.active-primary {
--n: var(--p);
--nc: var(--pc);
@apply active;
}
.link-primary {
@apply underline-offset-4 hover:underline;
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import "./styles.scss";
import "./main.css";
import { createApp, App as VueApp } from "vue";
import App from "./App.vue";
+7 -1
View File
@@ -1,5 +1,11 @@
import { describe, expect, test } from "vitest";
import { describe, expect, test, vi } from "vitest";
import { Container } from "./Container";
vi.mock("@/stores/config", () => ({
__esModule: true,
default: { base: "", hosts: [{ name: "localhost", id: "localhost" }] },
}));
describe("Container", () => {
const names = [
[
+40 -6
View File
@@ -1,16 +1,28 @@
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;
const hosts = computed(() =>
config.hosts.reduce(
(acc, item) => {
acc[item.id] = item;
return acc;
},
{} as Record<string, { name: string; id: string }>,
),
);
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,
@@ -23,8 +35,9 @@ export class Container {
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) {
@@ -34,11 +47,32 @@ export class Container {
}
}
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 hostLabel() {
return hosts.value[this.host]?.name;
}
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;
}
}
}
+11 -15
View File
@@ -5,10 +5,6 @@ import SimpleLogItem from "@/components/LogViewer/SimpleLogItem.vue";
import DockerEventLogItem from "@/components/LogViewer/DockerEventLogItem.vue";
import SkippedEntriesLogItem from "@/components/LogViewer/SkippedEntriesLogItem.vue";
export interface HasComponent {
getComponent(): Component;
}
export type JSONValue = string | number | boolean | JSONObject | Array<JSONValue>;
export type JSONObject = { [x: string]: JSONValue };
export type Position = "start" | "end" | "middle" | undefined;
@@ -22,7 +18,7 @@ export interface LogEvent {
readonly s: "stdout" | "stderr" | "unknown";
}
export abstract class LogEntry<T extends string | JSONObject> implements HasComponent {
export abstract class LogEntry<T extends string | JSONObject> {
protected readonly _message: T;
constructor(
message: T,
@@ -102,7 +98,7 @@ export class DockerEventLogEntry extends LogEntry<string> {
constructor(
message: string,
date: Date,
public readonly event: string,
public readonly event: "container-stopped" | "container-started",
) {
super(message, date.getTime(), date, "stderr", "info");
}
@@ -148,7 +144,15 @@ export class SkippedLogsEntry extends LogEntry<string> {
}
export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
if (typeof event.m === "string") {
if (isObject(event.m)) {
return new ComplexLogEntry(
event.m,
event.id,
new Date(event.ts),
event.l,
event.s === "unknown" ? "stderr" : event.s ?? "stderr",
);
} else {
return new SimpleLogEntry(
event.m,
event.id,
@@ -157,13 +161,5 @@ export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
event.p,
event.s === "unknown" ? "stderr" : event.s ?? "stderr",
);
} else {
return new ComplexLogEntry(
event.m,
event.id,
new Date(event.ts),
event.l,
event.s === "unknown" ? "stderr" : event.s ?? "stderr",
);
}
}
-15
View File
@@ -1,15 +0,0 @@
import { type App } from "vue";
import { Autocomplete, Button, Dropdown, Switch, Skeleton, Field, Modal, Config } from "@oruga-ui/oruga-next";
import { bulmaConfig } from "@oruga-ui/theme-bulma";
export const install = (app: App) => {
app
.use(Autocomplete)
.use(Button)
.use(Dropdown)
.use(Switch)
.use(Modal)
.use(Field)
.use(Skeleton)
.use(Config, bulmaConfig);
};
+13 -14
View File
@@ -1,20 +1,19 @@
import { type App } from "vue";
import { createI18n } from "vue-i18n";
import messages from "@intlify/unplugin-vue-i18n/messages";
const locale = messages?.hasOwnProperty(navigator.language) ? navigator.language : navigator.language.slice(0, 2);
const i18n = createI18n({
legacy: false,
locale: locale,
fallbackLocale: "en",
messages,
});
export const install = (app: App) => {
const messages = Object.fromEntries(
Object.entries(import.meta.glob<{ default: any }>("../../locales/*.y(a)?ml", { eager: true })).map(
([key, value]) => {
const yaml = key.endsWith(".yaml");
return [key.slice(14, yaml ? -5 : -4), value.default];
},
),
);
const i18n = createI18n({
legacy: false,
locale: navigator.language.slice(0, 2),
fallbackLocale: "en",
messages,
});
app.use(i18n);
};
export default i18n;
+1 -2
View File
@@ -2,13 +2,12 @@ import { type App } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import pages from "~pages";
import { setupLayouts } from "virtual:generated-layouts";
import config from "@/stores/config";
export const install = (app: App) => {
const routes = setupLayouts(pages);
const router = createRouter({
history: createWebHistory(`${config.base}/`),
history: createWebHistory(withBase("/")),
routes,
});
+7 -8
View File
@@ -1,14 +1,13 @@
<template>
<div class="hero is-halfheight">
<div class="hero-body">
<div class="container has-text-centered">
<h1 class="title">
404.
<small class="subtitle">{{ $t("error.page-not-found") }}</small>
</h1>
<page-with-links>
<div class="hero min-h-screen bg-base-200">
<div class="hero-content text-center">
<div class="max-w-md">
<p class="py-6 text-2xl font-bold">{{ $t("error.page-not-found") }}</p>
</div>
</div>
</div>
</div>
</page-with-links>
</template>
<script lang="ts" setup>
+20 -11
View File
@@ -1,10 +1,17 @@
<template>
<search></search>
<log-container :id="id" show-title :scrollable="activeContainers.length > 0" v-if="currentContainer"></log-container>
<div v-else-if="ready" class="notification is-warning is-light m-6">
<h1 class="title">
{{ $t("error.container-not-found") }}
</h1>
<log-container
:id="id"
:show-title="true"
:scrollable="activeContainers.length > 0"
v-if="currentContainer"
></log-container>
<div v-else-if="ready" class="hero min-h-screen bg-base-200">
<div class="hero-content text-center">
<div class="max-w-md">
<p class="py-6 text-2xl font-bold">{{ $t("error.container-not-found") }}</p>
</div>
</div>
</div>
</template>
@@ -16,11 +23,13 @@ const { id } = defineProps<{ id: string }>();
const currentContainer = store.currentContainer($$(id));
const { activeContainers, ready } = storeToRefs(store);
setTitle("loading");
onMounted(() => {
setTitle(currentContainer.value?.name);
watchEffect(() => {
if (ready.value) {
if (currentContainer.value) {
setTitle(currentContainer.value.name);
} else {
setTitle("Not Found");
}
}
});
watchEffect(() => setTitle(currentContainer.value?.name));
</script>
+18
View File
@@ -0,0 +1,18 @@
<template>
<page-with-links>
<section>
<article class="prose" v-html="data.content" v-if="data"></article>
</section>
</page-with-links>
</template>
<script lang="ts" setup>
const { id } = defineProps<{ id: string }>();
const { data } = useFetch(() => withBase("/api/content/" + id), {
refetch: true,
})
.get()
.json<{ title: string; content: string }>();
</script>
<style lang="postcss" scoped></style>
+48 -142
View File
@@ -1,127 +1,51 @@
<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>
<page-with-links class="gap-16">
<section>
<div class="stats grid bg-base-lighter shadow">
<div class="stat">
<div class="stat-value">{{ runningContainers.length }} / {{ containers.length }}</div>
<div class="stat-title">{{ $t("label.running") }} / {{ $t("label.total-containers") }}</div>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title">{{ runningContainers.length }}</p>
<p class="heading">{{ $t("label.running") }}</p>
<div class="stat">
<div class="stat-value">{{ totalCpu }}%</div>
<div class="stat-title">{{ $t("label.total-cpu-usage") }}</div>
</div>
</div>
<div class="level-item has-text-centered" data-ci-skip>
<div>
<p class="title">{{ totalCpu }}%</p>
<p class="heading">{{ $t("label.total-cpu-usage") }}</p>
<div class="stat">
<div class="stat-value">{{ formatBytes(totalMem) }}</div>
<div class="stat-title">{{ $t("label.total-mem-usage") }}</div>
</div>
</div>
<div class="level-item has-text-centered" data-ci-skip>
<div>
<p class="title">{{ 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 class="stat">
<div class="stat-value">{{ version }}</div>
<div class="stat-title">{{ $t("label.dozzle-version") }}</div>
</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">
<mdi:light-magnify />
</span>
</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">
<distance-time :date="item.created"></distance-time>
</div>
</router-link>
</div>
</div>
<section>
<container-table :containers="runningContainers"></container-table>
</section>
</div>
</page-with-links>
</template>
<script lang="ts" setup>
import { useFuse } from "@vueuse/integrations/useFuse";
import { Container } from "@/models/Container";
const { t } = useI18n();
const { version } = config;
const containerStore = useContainerStore();
const { containers } = storeToRefs(containerStore);
const router = useRouter();
const { containers, ready } = storeToRefs(containerStore) as unknown as {
containers: Ref<Container[]>;
ready: Ref<boolean>;
};
const sort = $ref("running");
const query = ref("");
const mostRecentContainers = $computed(() => [...containers.value].sort((a, b) => +b.created - +a.created));
const mostRecentContainers = $computed(() => containers.value.toSorted((a, b) => +b.created - +a.created));
const runningContainers = $computed(() => mostRecentContainers.filter((c) => c.state === "running"));
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 },
@@ -130,55 +54,37 @@ useIntervalFn(
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 },
);
function onEnter() {
if (data.value.length > 0) {
const item = data.value[0];
router.push({ name: "container-id", params: { id: item.id } });
watchEffect(() => {
if (ready.value) {
setTitle(t("title.dashboard", { count: runningContainers.length }));
}
}
});
</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;
}
}
<style lang="postcss" scoped>
:deep(tr td) {
padding-top: 1em;
padding-bottom: 1em;
}
@media screen and (max-width: 768px) {
.pb-0-is-mobile {
padding-bottom: 0 !important;
}
.pt-0-is-mobile {
padding-top: 0 !important;
}
.stat > div {
@apply text-center;
}
.icon {
padding: 10px 3px;
.stat-value {
@apply font-light;
}
.stat-title {
@apply font-light;
}
.section + .section {
padding-top: 0;
}
</style>
+39 -49
View File
@@ -1,50 +1,39 @@
<template>
<div class="hero is-halfheight">
<div class="hero-body">
<div class="container">
<section class="columns is-centered section">
<div class="column is-4">
<div class="card">
<div class="card-content">
<form action="" method="post" @submit.prevent="onLogin" ref="form">
<div class="field">
<label class="label">{{ $t("label.username") }}</label>
<div class="control">
<input
class="input"
type="text"
name="username"
autocomplete="username"
v-model="username"
autofocus
/>
</div>
</div>
<div class="field">
<label class="label">{{ $t("label.password") }}</label>
<div class="control">
<input
class="input"
type="password"
name="password"
autocomplete="current-password"
v-model="password"
/>
</div>
<p class="help is-danger" v-if="error">{{ $t("error.invalid-auth") }}</p>
</div>
<div class="field is-grouped is-grouped-centered mt-5">
<p class="control">
<button class="button is-primary" type="submit">{{ $t("button.login") }}</button>
</p>
</div>
</form>
</div>
</div>
</div>
</section>
</div>
<div class="card w-96 flex-shrink-0 bg-base-lighter shadow-2xl">
<div class="card-body">
<form action="" method="post" @submit.prevent="onLogin" ref="form">
<div class="form-control">
<label class="label">
<span class="label-text"> {{ $t("label.username") }} </span>
</label>
<input
class="input input-bordered"
type="text"
name="username"
autocomplete="username"
v-model="username"
autofocus
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Password</span>
</label>
<input
class="input input-bordered"
type="password"
name="password"
autocomplete="current-password"
v-model="password"
/>
</div>
<label class="label text-red" v-if="error">
{{ $t("error.invalid-auth") }}
</label>
<div class="form-control mt-6">
<button class="btn btn-primary" type="submit">{{ $t("button.login") }}</button>
</div>
</form>
</div>
</div>
</template>
@@ -60,14 +49,15 @@ let password = $ref("");
let form: HTMLFormElement | undefined = $ref();
async function onLogin() {
const response = await fetch(`${config.base}/api/validateCredentials`, {
const url = config.authProvider === "simple" ? "/api/token" : "/api/validateCredentials";
const response = await fetch(withBase(url), {
body: new FormData(form),
method: "post",
method: "POST",
});
if (response.status == 200) {
error = false;
window.location.href = `${config.base}/`;
window.location.href = withBase("/");
} else {
error = true;
}
+69 -159
View File
@@ -1,208 +1,118 @@
<template>
<div>
<section class="section">
<page-with-links>
<section>
<div class="has-underline">
<h2 class="title is-4">{{ $t("settings.about") }}</h2>
<h2>{{ $t("settings.about") }}</h2>
</div>
<div>
<span v-html="$t('settings.using-version', { version: currentVersion })"></span>
<span v-html="$t('settings.using-version', { version: config.version })"></span>
<div
v-if="hasUpdate"
v-html="$t('settings.update-available', { nextVersion: nextRelease.name, href: nextRelease.html_url })"
v-html="$t('settings.update-available', { nextVersion: latest?.name, href: latest?.htmlUrl })"
></div>
</div>
</section>
<section class="section">
<section class="flex flex-col gap-4">
<div class="has-underline">
<h2 class="title is-4">{{ $t("settings.display") }}</h2>
<h2>{{ $t("settings.display") }}</h2>
</div>
<div class="item">
<o-switch v-model="smallerScrollbars"> {{ $t("settings.small-scrollbars") }} </o-switch>
<div>
<toggle v-model="smallerScrollbars"> {{ $t("settings.small-scrollbars") }} </toggle>
</div>
<div class="item">
<o-switch v-model="showTimestamp"> {{ $t("settings.show-timesamps") }} </o-switch>
<div>
<toggle v-model="showTimestamp">{{ $t("settings.show-timesamps") }}</toggle>
</div>
<div class="item">
<o-switch v-model="showStd"> {{ $t("settings.show-std") }} </o-switch>
<div>
<toggle v-model="showStd">{{ $t("settings.show-std") }}</toggle>
</div>
<div class="item">
<o-switch v-model="softWrap"> {{ $t("settings.soft-wrap") }}</o-switch>
<div>
<toggle v-model="softWrap">{{ $t("settings.soft-wrap") }}</toggle>
</div>
<div class="item">
<div class="columns is-vcentered">
<div class="column is-narrow">
<o-field>
<o-dropdown v-model="hourStyle" aria-role="list">
<template #trigger>
<o-button variant="primary" type="button">
<span class="is-capitalized">{{ hourStyle }}</span>
<span class="icon">
<carbon:caret-down />
</span>
</o-button>
</template>
<o-dropdown-item :value="value" aria-role="listitem" v-for="value in ['auto', '12', '24']" :key="value">
<span class="is-capitalized">{{ value }}</span>
</o-dropdown-item>
</o-dropdown>
</o-field>
</div>
<div class="column">
{{ $t("settings.12-24-format") }}
</div>
</div>
<div class="flex items-center gap-6">
<dropdown-menu
v-model="hourStyle"
:options="[
{ label: 'Auto', value: 'auto' },
{ label: '12', value: '12' },
{ label: '24', value: '24' },
]"
/>
{{ $t("settings.12-24-format") }}
</div>
<div class="item">
<div class="columns is-vcentered">
<div class="column is-narrow">
<o-field>
<o-dropdown v-model="size" aria-role="list">
<template #trigger>
<o-button variant="primary" type="button">
<span class="is-capitalized">{{ size }}</span>
<span class="icon">
<carbon:caret-down />
</span>
</o-button>
</template>
<o-dropdown-item
:value="value"
aria-role="listitem"
v-for="value in ['small', 'medium', 'large']"
:key="value"
>
<span class="is-capitalized">{{ value }}</span>
</o-dropdown-item>
</o-dropdown>
</o-field>
</div>
<div class="column">{{ $t("settings.font-size") }}</div>
</div>
<div class="flex items-center gap-6">
<dropdown-menu
v-model="size"
:options="[
{ label: 'Small', value: 'small' },
{ label: 'Medium', value: 'medium' },
{ label: 'Large', value: 'large' },
]"
/>
{{ $t("settings.font-size") }}
</div>
<div class="item">
<div class="columns is-vcentered">
<div class="column is-narrow">
<o-field>
<o-dropdown v-model="lightTheme" aria-role="list">
<template #trigger>
<o-button variant="primary" type="button">
<span class="is-capitalized">{{ lightTheme }}</span>
<span class="icon">
<carbon:caret-down />
</span>
</o-button>
</template>
<o-dropdown-item
:value="value"
aria-role="listitem"
v-for="value in ['auto', 'dark', 'light']"
:key="value"
>
<span class="is-capitalized">{{ value }}</span>
</o-dropdown-item>
</o-dropdown>
</o-field>
</div>
<div class="column">{{ $t("settings.color-scheme") }}</div>
</div>
<div class="flex items-center gap-6">
<dropdown-menu
v-model="lightTheme"
:options="[
{ label: 'Auto', value: 'auto' },
{ label: 'Dark', value: 'dark' },
{ label: 'Light', value: 'light' },
]"
/>
{{ $t("settings.color-scheme") }}
</div>
</section>
<section class="section">
<section class="flex flex-col gap-2">
<div class="has-underline">
<h2 class="title is-4">{{ $t("settings.options") }}</h2>
<h2>{{ $t("settings.options") }}</h2>
</div>
<div>
<toggle v-model="search">
<div>{{ $t("settings.search") }} <key-shortcut char="f" class="align-top"></key-shortcut></div>
</toggle>
</div>
<div class="item">
<o-switch v-model="search">
<span v-html="$t('settings.search')"></span>
</o-switch>
<div>
<toggle v-model="showAllContainers">{{ $t("settings.show-stopped-containers") }}</toggle>
</div>
<div class="item">
<o-switch v-model="showAllContainers"> {{ $t("settings.show-stopped-containers") }} </o-switch>
<div>
<toggle v-model="automaticRedirect">{{ $t("settings.automatic-redirect") }}</toggle>
</div>
</section>
</div>
</page-with-links>
</template>
<script lang="ts" setup>
import {
search,
lightTheme,
smallerScrollbars,
showTimestamp,
showStd,
automaticRedirect,
hourStyle,
lightTheme,
search,
showAllContainers,
showStd,
showTimestamp,
size,
smallerScrollbars,
softWrap,
} from "@/composables/settings";
} from "@/stores/settings";
const { t } = useI18n();
setTitle(t("title.settings"));
const currentVersion = config.version;
let nextRelease = $ref({ html_url: "", name: "" });
let hasUpdate = $ref(false);
async function fetchNextRelease() {
if (!["dev", "master"].includes(currentVersion)) {
const response = await fetch("https://api.github.com/repos/amir20/dozzle/releases/latest");
if (response.ok) {
const release = await response.json();
hasUpdate =
release.tag_name.slice(1).localeCompare(currentVersion, undefined, { numeric: true, sensitivity: "base" }) > 0;
nextRelease = release;
}
} else {
hasUpdate = true;
nextRelease = {
html_url: "",
name: "master",
};
}
}
fetchNextRelease();
const { latest, hasUpdate } = useReleases();
</script>
<style lang="scss" scoped>
.title {
color: var(--title-color);
}
a.next-release {
text-decoration: underline;
&:hover {
text-decoration: none;
}
}
.section {
padding: 1rem 1.5rem;
}
<style lang="postcss" scoped>
.has-underline {
border-bottom: 1px solid var(--border-color);
padding: 1em 0px;
margin-bottom: 1em;
@apply mb-4 border-b border-base-content/50 py-4;
}
.item {
padding: 1em 0;
}
code {
border-radius: 4px;
background-color: #444;
:deep(a:not(.menu a)) {
@apply text-primary underline-offset-4 hover:underline;
}
</style>
+14 -1
View File
@@ -1,3 +1,5 @@
import { type Settings } from "@/stores/settings";
const text = document.querySelector("script#config__json")?.textContent || "{}";
interface Config {
@@ -8,6 +10,15 @@ interface Config {
maxLogs: number;
hostname: string;
hosts: { name: string; id: string }[];
authProvider: "simple" | "none" | "forward-proxy";
user?: {
username: string;
email: string;
name: string;
avatar: string;
};
serverSettings?: Settings;
pages?: { id: string; title: string }[];
}
const pageConfig = JSON.parse(text);
@@ -19,4 +30,6 @@ const config: Config = {
config.version = config.version.replace(/^v/, "");
export default config;
export default Object.freeze(config);
export const withBase = (path: string) => `${config.base}${path}`;
+44 -6
View File
@@ -2,6 +2,11 @@ import { acceptHMRUpdate, defineStore } from "pinia";
import { Ref, UnwrapNestedRefs } from "vue";
import type { ContainerHealth, ContainerJson, ContainerStat } from "@/types/Container";
import { Container } from "@/models/Container";
import i18n from "@/modules/i18n";
const { showToast } = useToast();
// @ts-ignore
const { t } = i18n.global;
export const useContainerStore = defineStore("container", () => {
const containers: Ref<Container[]> = ref([]);
@@ -29,7 +34,20 @@ export const useContainerStore = defineStore("container", () => {
function connect() {
es?.close();
ready.value = false;
es = new EventSource(`${config.base}/api/events/stream`);
es = new EventSource(withBase("/api/events/stream"));
es.addEventListener("error", (e) => {
if (es?.readyState === EventSource.CLOSED) {
showToast(
{
id: "events-stream",
message: t("error.events-stream.message"),
title: t("error.events-stream.title"),
type: "error",
},
{ once: true },
);
}
});
es.addEventListener("containers-changed", (e: Event) =>
updateContainers(JSON.parse((e as MessageEvent).data) as ContainerJson[]),
@@ -39,7 +57,7 @@ export const useContainerStore = defineStore("container", () => {
const container = allContainersById.value[stat.id] as unknown as UnwrapNestedRefs<Container>;
if (container) {
const { id, ...rest } = stat;
container.stat = rest;
container.updateStat(rest);
}
});
es.addEventListener("container-die", (e) => {
@@ -58,11 +76,33 @@ export const useContainerStore = defineStore("container", () => {
}
});
es.onopen = () => {
if (containers.value.length > 0) {
containers.value = [];
}
};
watchOnce(containers, () => (ready.value = true));
}
connect();
(async function () {
try {
await until(ready).toBe(true, { timeout: 8000, throwOnTimeout: true });
} catch (e) {
showToast(
{
id: "events-timeout",
message: t("error.events-timeout.message"),
title: t("error.events-timeout.title"),
type: "error",
},
{ once: true },
);
}
})();
const updateContainers = (containersPayload: ContainerJson[]) => {
const existingContainers = containersPayload.filter((c) => allContainersById.value[c.id]);
const newContainers = containersPayload.filter((c) => !allContainersById.value[c.id]);
@@ -93,8 +133,8 @@ export const useContainerStore = defineStore("container", () => {
};
const currentContainer = (id: Ref<string>) => computed(() => allContainersById.value[id.value]);
const appendActiveContainer = ({ id }: Container) => activeContainerIds.value.push(id);
const removeActiveContainer = ({ id }: Container) =>
const appendActiveContainer = ({ id }: { id: string }) => activeContainerIds.value.push(id);
const removeActiveContainer = ({ id }: { id: string }) =>
activeContainerIds.value.splice(activeContainerIds.value.indexOf(id), 1);
return {
@@ -110,8 +150,6 @@ export const useContainerStore = defineStore("container", () => {
};
});
// @ts-ignore
if (import.meta.hot) {
// @ts-ignore
import.meta.hot.accept(acceptHMRUpdate(useContainerStore, import.meta.hot));
}
+29
View File
@@ -0,0 +1,29 @@
const { data: releases } = useFetch(withBase("/api/releases")).get().json<
{
name: string;
mentionsCount: number;
createdAt: string;
body: string;
tag: string;
htmlUrl: string;
latest: boolean;
features: number;
bugFixes: number;
breaking: number;
}[]
>();
const hasUpdate = computed(() => {
if (!releases.value?.length) return false;
return releases.value[0].tag !== config.version;
});
const latest = computed(() => releases.value?.find((release) => release.latest));
export function useReleases() {
return {
hasUpdate,
latest,
releases,
};
}
+58
View File
@@ -0,0 +1,58 @@
import { toRefs } from "@vueuse/core";
const DOZZLE_SETTINGS_KEY = "DOZZLE_SETTINGS";
export type Settings = {
search: boolean;
size: "small" | "medium" | "large";
menuWidth: number;
smallerScrollbars: boolean;
showTimestamp: boolean;
showStd: boolean;
showAllContainers: boolean;
lightTheme: "auto" | "dark" | "light";
hourStyle: "auto" | "24" | "12";
softWrap: boolean;
collapseNav: boolean;
automaticRedirect: boolean;
};
export const DEFAULT_SETTINGS: Settings = {
search: true,
size: "medium",
menuWidth: 15,
smallerScrollbars: false,
showTimestamp: true,
showStd: false,
showAllContainers: false,
lightTheme: "auto",
hourStyle: "auto",
softWrap: true,
collapseNav: false,
automaticRedirect: true,
};
export const settings = useStorage(DOZZLE_SETTINGS_KEY, DEFAULT_SETTINGS);
settings.value = { ...DEFAULT_SETTINGS, ...settings.value, ...config.serverSettings };
if (config.user) {
watch(settings, (value) => {
fetch(withBase("/api/profile/settings"), {
method: "PUT",
body: JSON.stringify(value),
});
});
}
export const {
collapseNav,
softWrap,
hourStyle,
lightTheme,
showAllContainers,
showTimestamp,
showStd,
smallerScrollbars,
menuWidth,
size,
search,
automaticRedirect,
} = toRefs(settings);
-243
View File
@@ -1,243 +0,0 @@
@charset "utf-8";
@import "bulma/sass/utilities/initial-variables.sass";
$body-background-color: var(--body-background-color);
$scheme-main: var(--scheme-main);
$scheme-main-bis: var(--scheme-main-bis);
$scheme-main-ter: var(--scheme-main-ter);
$border: var(--border-color);
$border-hover: var(--border-hover-color);
$menu-item-active-background-color: var(--menu-item-active-background-color);
$menu-item-color: var(--menu-item-color);
$menu-item-hover-background-color: var(--menu-item-hover-background-color);
$menu-item-hover-color: var(--menu-item-hover-color);
$text-strong: var(--text-strong-color);
$text: var(--text-color);
$text-light: var(--text-light-color);
$panel-heading-background-color: var(--panel-heading-background-color);
$panel-heading-color: var(--panel-heading-color);
$link: $turquoise;
$link-active: $grey-dark;
$dark-toolbar-color: rgba($black-bis, 0.7);
$light-toolbar-color: rgba($grey-darker, 0.7);
@import "bulma/bulma.sass";
@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/dropdown.scss";
@import "@oruga-ui/theme-bulma/dist/scss/components/skeleton.scss";
@import "splitpanes/dist/splitpanes.css";
@mixin dark {
--scheme-main: #{$black};
--scheme-main-bis: #{$black-bis};
--scheme-main-ter: #{$black-ter};
--border-color: #{$grey-darker};
--border-hover-color: var(--secondary-color);
--logo-color: var(--secondary-color);
--primary-color: #{$turquoise};
--secondary-color: #{$yellow};
--body-background-color: #{$black-bis};
--action-toolbar-background-color: #{$dark-toolbar-color};
--body-color: #{$grey-lighter};
--menu-item-active-background-color: var(--primary-color);
--menu-item-color: hsl(0, 6%, 87%);
--menu-item-hover-background-color: #{$white-ter};
--menu-item-hover-color: #{$black-ter};
--panel-heading-background-color: var(--secondary-color);
--panel-heading-color: var(--scheme-main-bis);
--text-strong-color: #{$grey-lightest};
--text-color: #{$grey-lighter};
--text-light-color: #{$grey};
}
@mixin light {
--scheme-main: #{$white};
--scheme-main-bis: #{$white-bis};
--scheme-main-ter: #{$white-ter};
--border-color: #{$grey-lighter};
--border-hover-color: var(--secondary-color);
--logo-color: var(--secondary-color);
--primary-color: #{$turquoise};
--secondary-color: rgb(249 115 22);
--body-background-color: #{$white-bis};
--action-toolbar-background-color: #{$light-toolbar-color};
--body-color: #{$grey-darker};
--menu-item-active-background-color: var(--primary-color);
--menu-item-color: #{$grey-dark};
--menu-item-hover-background-color: #eee8e7;
--menu-item-hover-color: #{$black-ter};
--panel-heading-background-color: var(--secondary-color);
--panel-heading-color: var(--text-strong-color);
--text-strong-color: #{$grey-dark};
--text-color: #{$grey-darker};
--text-light-color: #{$grey};
}
[data-theme="dark"] {
@include dark;
}
[data-theme="light"] {
@include light;
}
@media (prefers-color-scheme: dark) {
html {
@include dark;
}
}
@media (prefers-color-scheme: light) {
html {
@include light;
}
}
: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;
scroll-snap-type: y proximity;
}
html.has-custom-scrollbars {
::-webkit-scrollbar {
width: 8px;
display: content;
}
::-webkit-scrollbar-thumb {
background-color: rgba(128, 128, 128, 0.33);
outline: 1px solid slategrey;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:active {
background-color: #777;
}
::-webkit-scrollbar-track {
background-color: transparent;
}
::-webkit-scrollbar-track:hover {
background-color: rgba(64, 64, 64, 0.33);
}
section main {
scrollbar-color: #353535 transparent;
scrollbar-width: thin;
}
}
@media screen and (min-width: 770px) {
.splitpanes__pane {
overflow: unset;
}
}
@media screen and (max-device-width: 480px) {
body {
-webkit-text-size-adjust: 100%;
}
}
.splitpanes__splitter {
z-index: 99;
}
.is-ellipsis {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.modal {
z-index: 1000;
.modal-background {
backdrop-filter: blur(4px);
}
}
.button .button-wrapper > span {
display: contents;
}
.has-dropshadow {
filter: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow(0 1px 1px rgb(0 0 0 / 0.06));
}
.has-boxshadow {
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
}
mark {
border-radius: 2px;
background-color: var(--secondary-color);
animation: pops 200ms ease-out;
display: inline-block;
}
@keyframes pops {
0% {
transform: scale(1.5);
}
100% {
transform: scale(1.05);
}
}
.button.is-rounded:hover {
color: var(--text-strong-color);
background: var(--scheme-main-ter);
}
+14
View File
@@ -37,3 +37,17 @@ 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;
}
+1 -1
View File
@@ -61,7 +61,7 @@ services:
playwright:
container_name: playwright
image: mcr.microsoft.com/playwright:v1.35.1-jammy
image: mcr.microsoft.com/playwright:v1.39.0-jammy
working_dir: /app
volumes:
- .:/app
+3
View File
@@ -64,8 +64,11 @@ export default defineConfig({
text: "Advanced Configuration",
items: [
{ text: "Authentication", link: "/guide/authentication" },
{ text: "Changing Base", link: "/guide/changing-base" },
{ text: "Healthcheck", link: "/guide/healthcheck" },
{ text: "Hostname", link: "/guide/hostname" },
{ text: "Remote Hosts", link: "/guide/remote-hosts" },
{ text: "Custom Documentation", link: "/guide/custom-documentation" },
{ 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" },
@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { useMutationObserver } from "@vueuse/core";
import { onMounted, ref } from "vue";
import { onMounted, onUnmounted, ref } from "vue";
const isDark = ref(false);
onMounted(() => {
@@ -16,6 +16,14 @@ onMounted(() => {
},
);
});
onMounted(() => {
document.documentElement.classList.add("home");
});
onUnmounted(() => {
document.documentElement.classList.remove("home");
});
</script>
<template>
+8 -2
View File
@@ -144,14 +144,20 @@
}
.VPHomeHero .name {
@apply font-playfair font-light text-7xl;
@apply text-7xl font-light;
font-family: "Playfair Display", serif;
}
.VPNavBarTitle .title {
@apply font-playfair font-light text-3xl;
@apply text-3xl font-light;
color: var(--bar-title-color);
font-family: "Playfair Display", serif;
}
.VPHero .container .image {
max-width: 1200px;
}
.home .VPNav .container {
max-width: 100%;
}
-30
View File
@@ -1,30 +0,0 @@
import { Handler, HandlerEvent, HandlerContext } from "@netlify/functions";
let cache;
let cacheTime = 0;
const handler: Handler = async (event: HandlerEvent, context: HandlerContext) => {
if (cache && Date.now() - cacheTime < 1000 * 60 * 10) {
const headers = {
"x-cache": "HIT",
};
return {
headers,
statusCode: 200,
body: JSON.stringify(cache),
};
}
const response = await fetch("https://hub.docker.com/v2/repositories/amir20/dozzle");
const data = await response.json();
const { full_description, ...rest } = data;
cache = rest;
cacheTime = Date.now();
return {
statusCode: 200,
body: JSON.stringify(rest),
};
};
export { handler };
+224
View File
@@ -4,6 +4,230 @@ title: Authentication
# Setting Up Authentication
Dozzle support two configurations for authentication. In the first configuration, you bring your own authentication method by protecting Dozzle through a proxy. Dozzle can read appropriate headers out of the box.
If you do not have an authentication solution then Dozzle has a simple file based user management solution. Authentication providers are setup using `--auth-provider` flag. In both of these configurations, Dozzle will try to save user settings to disk. This data is written to `/data`.
## Forward Proxy
Dozzle can be configured to read proxy headers by setting `--auth-provider` to `forward-proxy`.
::: code-group
```sh [cli]
$ docker run -v /var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir20/dozzle --auth-provider forward-proxy
```
```yaml [docker-compose.yml]
version: "3"
services:
dozzle:
image: amir20/dozzle:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
ports:
- 8080:8080
environment:
DOZZLE_AUTH_PROVIDER: forward-proxy
```
:::
In this mode, Dozzle expects the following headers:
- `Remote-User` to map to the username eg. `johndoe`
- `Remote-Email` to map to the user's email address. This email is also used to find the right [Gravatar](https://gravatar.com/) for the user.
- `Remote-Name` to be a display name like `John Doe`
### Setting up Dozzle with Authelia
[Authelia](https://www.authelia.com/) is an open-source authentication and authorization server and portal fulfilling the identity and access management. While setting up Authelia is out of scope for this section, the configuration can be shared as an example for setting up Dozzle with Authelia.
::: code-group
```yaml [docker-compose.yml]
version: "3.3"
networks:
net:
driver: bridge
services:
authelia:
image: authelia/authelia
container_name: authelia
volumes:
- ./authelia:/config
networks:
- net
labels:
- "traefik.enable=true"
- "traefik.http.routers.authelia.rule=Host(`authelia.example.com`)"
- "traefik.http.routers.authelia.entrypoints=https"
- "traefik.http.routers.authelia.tls=true"
- "traefik.http.routers.authelia.tls.options=default"
- "traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/verify?rd=https://authelia.example.com"
- "traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true"
- "traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email"
expose:
- 9091
restart: unless-stopped
traefik:
image: traefik:2.10.5
container_name: traefik
volumes:
- ./traefik:/etc/traefik
- /var/run/docker.sock:/var/run/docker.sock
networks:
- net
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(`traefik.example.com`)"
- "traefik.http.routers.api.entrypoints=https"
- "traefik.http.routers.api.service=api@internal"
- "traefik.http.routers.api.tls=true"
- "traefik.http.routers.api.tls.options=default"
- "traefik.http.routers.api.middlewares=authelia@docker"
ports:
- "80:80"
- "443:443"
command:
- "--api"
- "--providers.docker=true"
- "--providers.docker.exposedByDefault=false"
- "--providers.file.filename=/etc/traefik/certificates.yml"
- "--entrypoints.http=true"
- "--entrypoints.http.address=:80"
- "--entrypoints.http.http.redirections.entrypoint.to=https"
- "--entrypoints.http.http.redirections.entrypoint.scheme=https"
- "--entrypoints.https=true"
- "--entrypoints.https.address=:443"
- "--log=true"
- "--log.level=DEBUG"
dozzle:
image: amir20/dozzle:latest
networks:
- net
environment:
DOZZLE_AUTH_PROVIDER: forward-proxy
volumes:
- /var/run/docker.sock:/var/run/docker.sock
labels:
- "traefik.enable=true"
- "traefik.http.routers.dozzle.rule=Host(`dozzle.example.com`)"
- "traefik.http.routers.dozzle.entrypoints=https"
- "traefik.http.routers.dozzle.tls=true"
- "traefik.http.routers.dozzle.tls.options=default"
- "traefik.http.routers.dozzle.middlewares=authelia@docker"
expose:
- 8080
restart: unless-stopped
```
```yaml [configuration.yml]
###############################################################
# Authelia configuration #
###############################################################
jwt_secret: a_very_important_secret
default_redirection_url: https://public.example.com
server:
host: 0.0.0.0
port: 9091
log:
level: info
totp:
issuer: authelia.com
authentication_backend:
file:
path: /config/users_database.yml
access_control:
default_policy: deny
rules:
- domain: traefik.example.com
policy: one_factor
- domain: dozzle.example.com
policy: one_factor
session:
secret: unsecure_session_secret
domain: example.com # Should match whatever your root protected domain is
regulation:
max_retries: 3
find_time: 120
ban_time: 300
storage:
encryption_key: you_must_generate_a_random_string_of_more_than_twenty_chars_and_configure_this
local:
path: /config/db.sqlite3
notifier:
filesystem:
filename: /config/notification.txt
```
:::
Valid SSL keys are required because Authelia only supports SSL.
## File Based User Management
Dozzle supports multi-user authentication by setting `--auth-provider` to `simple`. In this mode, Dozzle will try to read `/data/users.yml`. The content of the file looks like
```yml
users:
# "admin" here is username
admin:
name: "Admin"
# Just sha-256 which can be computed with "echo -n password | shasum -a 256"
password: "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
email: me@email.net
```
Dozzle uses `email` to generate avatars using [Gravatar](https://gravatar.com/). It is optional.
The password is hashed using `sha256` which can be generated with `echo -n "secret-password" | shasum -a 256` or `echo -n "secret-password" | sha256sum` on linux.
You will need to mount this file for Dozzle to find it. Here is an example:
::: code-group
```sh [cli]
$ docker run -v /var/run/docker.sock:/var/run/docker.sock -v /path/to/dozzle/data:/data -p 8080:8080 amir20/dozzle --auth-provider simple
```
```yaml [docker-compose.yml]
version: "3"
services:
dozzle:
image: amir20/dozzle:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /path/to/dozzle/data:/data
ports:
- 8080:8080
environment:
DOZZLE_AUTH_PROVIDER: simple
```
:::
Dozzle uses [JWT](https://en.wikipedia.org/wiki/JSON_Web_Token) to generate tokens for authentication. This token is saved in a cookie.
## Single Username/Password
::: danger
`--username` and `--password` flags will be removed in v6.x in favor of `--auth-provider`.
:::
Dozzle supports a very simple authentication out of the box with just username and password. You should deploy using SSL to keep the credentials safe. See configuration to use `--username` and `--password`. You can also use docker secrets `--usernamefile` and `--passwordfile`.
::: code-group

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