Compare commits

...

170 Commits

Author SHA1 Message Date
Amir Raminfar 2709417ba3 Release 4.8.2 2023-03-23 16:12:38 -07:00
Amir Raminfar c4386d2d84 Updates modules 2023-03-23 12:55:43 -07:00
Amir Raminfar fbcf989885 Updates with dark and light video 2023-03-21 15:25:03 -07:00
Amir Raminfar 07352f6e16 Adds dark and light for browser 2023-03-21 12:41:22 -07:00
Amir Raminfar 6fecfce3c7 Updates color on video 2023-03-21 12:35:32 -07:00
Amir Raminfar 2884ffafeb Adds blur 2023-03-21 12:33:06 -07:00
Amir Raminfar 67ac202b04 Updates videos 2023-03-20 13:08:18 -07:00
Amir Raminfar 839afd8fcf Fixes bug for level 2023-03-20 13:06:31 -07:00
Amir Raminfar 202b07bfb4 Updates logic to find ERROR in logs 2023-03-20 11:13:53 -07:00
renovate[bot] 15bd0999a2 Update dependency vite to v4.2.1 (#2088) 2023-03-20 06:08:57 -07:00
Amir Raminfar 4450c2527f node latest (#2089)
* Fixes tests

* Fixes tests

* Updates tests

* Updates node to latest

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

* Updates prettier ignore

* Updates formatting

* Fixes tests

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

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

* migrates to vitepress

* Remove api link

* Adds gitignore

* Adds gitignore

* Removes unused colors

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

* Adds UI for drop down

* Adds support for TLS and remove SSH

* Changes dropdown to only show up with >1 hosts

* Fixes js tests

* Fixes go tests

* Fixes download link

* Updates readme

* Removes unused imports

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

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

* Updates diff

* Adds log class for level

* Groups messages by level

* Fixes bugs for grouping

* Fixes tests

* Fixes json bug

* Updates logic to support other kind of levels

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-07 13:39:14 -08:00
dependabot[bot] 88517ff0f7 Bump github.com/opencontainers/image-spec from 1.0.1 to 1.0.2 (#2005) 2023-01-07 12:25:33 -08:00
renovate[bot] 6dd3758f98 Update dependency jsdom to v21 (#2003) 2023-01-07 09:58:42 -08:00
Amir Raminfar e3630465ed Updates deps 2023-01-06 09:59:26 -08:00
renovate[bot] 4715f18c07 Update cypress/included Docker tag to v12.3.0 (#2002)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-06 09:46:19 -08:00
Amir Raminfar b1d4520360 Updates deps 2023-01-03 10:04:13 -08:00
renovate[bot] 467b5e8e19 Update dependency unplugin-icons to ^0.15.0 (#2001)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-03 10:03:27 -08:00
Amir Raminfar ce39d3837f Release 4.6.2 2023-01-02 10:06:09 -08:00
Amir Raminfar 884dd94242 Adds analytics for requests made (#1999) 2023-01-02 09:28:27 -08:00
Amir Raminfar df2ff662dd Removes outdated i18n plugin (#1998) 2022-12-29 08:27:54 -08:00
renovate[bot] f602d4e69d Update dependency vite to v4.0.3 (#1996) 2022-12-25 08:02:53 -08:00
renovate[bot] 27f898b696 Update cypress/included Docker tag to v12.2.0 (#1995) 2022-12-25 08:02:46 -08:00
renovate[bot] 400afddc45 Update dependency vitest to ^0.26.0 (#1994) 2022-12-20 07:06:03 -08:00
Amir Raminfar edf7edced3 Release 4.6.1 2022-12-18 14:44:29 -08:00
Amir Raminfar 0ef5c36900 Fixes #1990 by only coming list when relative fields change (#1993) 2022-12-18 14:38:29 -08:00
renovate[bot] a008ede927 Update dependency vite to v4.0.2 (#1992)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-18 14:18:39 -08:00
renovate[bot] b4e88937cc Update module github.com/docker/docker to v20.10.22+incompatible (#1989)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-18 14:18:32 -08:00
renovate[bot] fd3992cabd Update cypress/included Docker tag to v12.1.0 (#1984)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-14 11:00:42 -08:00
renovate[bot] 47bed89fe2 Update dependency vite to v4.0.1 (#1979)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-12 13:31:39 -08:00
Amir Raminfar 361fff7803 Release 4.6.0 2022-12-12 09:38:17 -08:00
Amir Raminfar a6ba94e03f Adds hostname config for setting UI parameter (#1983)
* Adds hostname config to setting a hostname in title

* Also updates title for hostname
2022-12-12 09:37:54 -08:00
renovate[bot] d39e2514b2 Update dependency vite-plugin-pages to ^0.28.0 (#1982)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-12 09:14:35 -08:00
Amir Raminfar 155757dd74 Removes tail size (#1981)
* Removes tail size

* Removes tail from analytics
2022-12-12 09:14:29 -08:00
97 changed files with 4789 additions and 2279 deletions
+1 -1
View File
@@ -5,4 +5,4 @@ node_modules
dist
.git
e2e
docs
+8 -7
View File
@@ -1,10 +1,9 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
title: ""
labels: ""
assignees: amir20
---
**Describe the bug**
@@ -12,6 +11,7 @@ A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
@@ -24,9 +24,10 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS, Windows or Mac Os]
- Docker version [e.g. `docker version`'s output]
- Browser & version [e.g. chrome, safari]
- Version [e.g. 1.26.1. Can be found at `<Dozzle-host>/version`]
- OS: [e.g. iOS, Windows or Mac Os]
- Docker version [e.g. `docker version`'s output]
- Browser & version [e.g. chrome, safari]
- Version [e.g. 1.26.1. Can be found at `<Dozzle-host>/version`]
**If applicable include logs with `--level debug` and browser logs**
+1 -2
View File
@@ -1,10 +1,9 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
title: ""
labels: enhancement
assignees: amir20
---
**Is your feature request related to a problem? Please describe.**
+12 -8
View File
@@ -12,10 +12,12 @@ jobs:
uses: actions/checkout@v3
- name: Install Node
uses: actions/setup-node@v3
with:
node-version: latest
- name: Install pnpm
uses: pnpm/action-setup@v2.2.4
with:
version: 6.20.1
version: 7.26.0
- name: Install dependencies
run: pnpm install
- name: Run Tests
@@ -25,7 +27,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v3
uses: actions/setup-go@v4
with:
go-version: 1.19.x
- name: Checkout code
@@ -39,9 +41,9 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
- name: Build images
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml build
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker compose -f e2e/docker-compose.yml build
- name: Run tests
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml up --build --force-recreate --exit-code-from cypress
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker compose -f e2e/docker-compose.yml up --build --force-recreate --exit-code-from cypress
buildx:
needs: [go-test, npm-test, int-test]
name: Release
@@ -53,22 +55,22 @@ jobs:
with:
images: amir20/dozzle
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.2.1
uses: docker/setup-buildx-action@v2.5.0
- name: Login to DockerHub
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v3.2.0
uses: docker/build-push-action@v4.0.0
with:
push: true
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }}
build-args: TAG=${{ steps.meta.outputs.version }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=amir20/dozzle:latest
cache-to: type=inline
cache-from: type=gha
cache-to: type=gha,mode=max
git-release:
needs: [buildx]
name: Github Release
@@ -80,6 +82,8 @@ jobs:
fetch-depth: 0
- name: Install Node
uses: actions/setup-node@v3
with:
node-version: latest
- name: Install pnpm
uses: pnpm/action-setup@v2.2.4
with:
+4 -4
View File
@@ -18,19 +18,19 @@ jobs:
with:
images: amir20/dozzle
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.2.1
uses: docker/setup-buildx-action@v2.5.0
- name: Login to DockerHub
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v3.2.0
uses: docker/build-push-action@v4.0.0
with:
push: true
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }}
build-args: TAG=${{ steps.meta.outputs.version }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=amir20/dozzle:master
cache-to: type=inline
cache-from: type=gha
cache-to: type=gha,mode=max
+9 -7
View File
@@ -15,20 +15,22 @@ jobs:
uses: actions/checkout@v3
- name: Install Node
uses: actions/setup-node@v3
with:
node-version: latest
- name: Install pnpm
uses: pnpm/action-setup@v2.2.4
with:
version: 6.20.1
version: 7.26.0
- name: Install dependencies
run: pnpm install
- name: Run Tests
run: pnpm run test
run: pnpm run test -- --outputTruncateLength=500
go-test:
name: Go Tests
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v3
uses: actions/setup-go@v4
with:
go-version: 1.19.x
- name: Checkout code
@@ -44,7 +46,7 @@ jobs:
with:
fetch-depth: 2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.2.1
uses: docker/setup-buildx-action@v2.5.0
- name: Login to DockerHub
if: ${{ !github.event.repository.fork && !github.event.pull_request.head.repo.fork }}
uses: docker/login-action@v2.1.0
@@ -52,10 +54,10 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build images
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml build --build-arg BUILDKIT_INLINE_CACHE=1
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker compose -f e2e/docker-compose.yml build --build-arg BUILDKIT_INLINE_CACHE=1
- name: Push images
if: ${{ !github.event.repository.fork && !github.event.pull_request.head.repo.fork }}
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml push
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker compose -f e2e/docker-compose.yml push
- name: Set commit message for push
if: github.event_name == 'push'
run: |
@@ -69,4 +71,4 @@ jobs:
git log -1 --pretty=%B ${{github.event.pull_request.head.sha}} >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Run tests
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml up --build --force-recreate --exit-code-from cypress
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker compose -f e2e/docker-compose.yml up --build --force-recreate --exit-code-from cypress
+1
View File
@@ -7,3 +7,4 @@ dozzle
coverage
.pnpm-debug.log
.vscode
coverage.out
-1
View File
@@ -1 +0,0 @@
_
-4
View File
@@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname $0)/_/husky.sh"
pnpm lint-staged
+3
View File
@@ -0,0 +1,3 @@
shared-workspace-lockfile=false
include-workspace-root=true
ignore-workspace-root-check=true
+5
View File
@@ -0,0 +1,5 @@
auto-imports.d.ts
components.d.ts
docs/.vitepress/cache
docs/.vitepress/dist
dist
+13 -13
View File
@@ -14,22 +14,22 @@ appearance, race, religion, or sexual identity and orientation.
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
+4 -5
View File
@@ -3,16 +3,15 @@ FROM --platform=$BUILDPLATFORM node:19-alpine as node
RUN npm install -g pnpm
WORKDIR /build
# Install dependencies from lock file
COPY pnpm-lock.yaml ./
RUN pnpm fetch
COPY pnpm-*.yaml ./
RUN pnpm fetch --ignore-scripts --no-optional
# Copy package.json and install dependencies
COPY package.json ./
RUN pnpm install -r --offline --ignore-scripts
RUN pnpm install --offline --ignore-scripts --no-optional
# Copy assets and translations to build
COPY .* vite.config.ts index.html ./
@@ -22,7 +21,7 @@ COPY locales ./locales
# Build assets
RUN pnpm build
FROM --platform=$BUILDPLATFORM golang:1.19.4-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.20.2-alpine AS builder
RUN apk add --no-cache ca-certificates && mkdir /dozzle
+45 -26
View File
@@ -37,16 +37,7 @@ The simplest way to use dozzle is to run the docker container. Also, mount the D
Dozzle will be available at [http://localhost:8888/](http://localhost:8888/). You can change `-p 8888:8080` to any port. For example, if you want to view dozzle over port 4040 then you would do `-p 4040:8080`.
### With Docker swarm
docker service create \
--name=dozzle \
--publish=8888:8080 \
--constraint=node.role==manager \
--mount=type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
amir20/dozzle:latest
### With Docker compose
### Connecting with Docker compose
version: "3"
services:
@@ -58,8 +49,35 @@ Dozzle will be available at [http://localhost:8888/](http://localhost:8888/). Yo
ports:
- 9999:8080
### Connecting to remote hosts
### Enabling health check
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.
@@ -81,7 +99,6 @@ Dozzle doesn't enable healthcheck by default as it adds extra CPU usage. `health
retries: 5
start_period: 30s
#### Security
You can control the device Dozzle binds to by passing `--addr` parameter. For example,
@@ -119,19 +136,19 @@ If you do not want to be tracked at all, see the `--no-analytics` flag below.
Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can use the CLI flags or environment variables. The table below outlines all supported options and their respective env vars.
| Flag | Env Variable | Default |
| ---------------- | --------------------- | ------- |
| `--addr` | `DOZZLE_ADDR` | `:8080` |
| `--base` | `DOZZLE_BASE` | `/` |
| `--level` | `DOZZLE_LEVEL` | `info` |
| n/a | `DOCKER_API_VERSION` | not set |
| `--tailSize` | `DOZZLE_TAILSIZE` | `300` |
| `--filter` | `DOZZLE_FILTER` | `""` |
| `--username` | `DOZZLE_USERNAME` | `""` |
| `--password` | `DOZZLE_PASSWORD` | `""` |
| `--usernamefile` | `DOZZLE_USERNAME_FILE`| `""` |
| `--passwordfile` | `DOZZLE_PASSWORD_FILE`| `""` |
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
| Flag | Env Variable | Default |
| ---------------- | ---------------------- | ------- |
| `--addr` | `DOZZLE_ADDR` | `:8080` |
| `--base` | `DOZZLE_BASE` | `/` |
| `--hostname` | `DOZZLE_HOSTNAME` | `""` |
| `--level` | `DOZZLE_LEVEL` | `info` |
| `--filter` | `DOZZLE_FILTER` | `""` |
| `--username` | `DOZZLE_USERNAME` | `""` |
| `--password` | `DOZZLE_PASSWORD` | `""` |
| `--usernamefile` | `DOZZLE_USERNAME_FILE` | `""` |
| `--passwordfile` | `DOZZLE_PASSWORD_FILE` | `""` |
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
| `--remote-host` | `DOZZLE_REMOTE_HOST` | |
## Troubleshooting and FAQs
@@ -186,17 +203,19 @@ Dozzle has a [special route](https://github.com/amir20/dozzle/blob/master/assets
<details>
<summary>I installed Dozzle but memory consumption doesn't show up!</summary>
*This is an issue specific to ARM devices*
_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
```
+19 -1
View File
@@ -21,7 +21,25 @@ func SendStartEvent(se StartEvent) error {
},
}
jsonValue, err := json.Marshal(postBody)
return doRequest(postBody)
}
func SendRequestEvent(re RequestEvent) error {
postBody := map[string]interface{}{
"client_id": re.ClientId,
"events": []map[string]interface{}{
{
"name": "request",
"params": re,
},
},
}
return doRequest(postBody)
}
func doRequest(body map[string]interface{}) error {
jsonValue, err := json.Marshal(body)
if err != nil {
return err
}
+14 -7
View File
@@ -1,11 +1,18 @@
package analytics
type StartEvent struct {
ClientId string `json:"-"`
Version string `json:"version"`
FilterLength int `json:"filterLength"`
CustomAddress bool `json:"customAddress"`
CustomBase bool `json:"customBase"`
TailSize int `json:"tailSize"`
Protected bool `json:"protected"`
ClientId string `json:"-"`
Version string `json:"version"`
FilterLength int `json:"filterLength"`
RemoteHostLength int `json:"remoteHostLength"`
CustomAddress bool `json:"customAddress"`
CustomBase bool `json:"customBase"`
Protected bool `json:"protected"`
HasHostname bool `json:"hasHostname"`
}
type RequestEvent struct {
ClientId string `json:"-"`
TotalContainers int `json:"totalContainers"`
RunningContainers int `json:"runningContainers"`
}
+19 -3
View File
@@ -1,4 +1,7 @@
// Generated by 'unplugin-auto-import'
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-auto-import
export {}
declare global {
const $$: typeof import('vue/macros')['$$']
@@ -103,10 +106,10 @@ declare global {
const refThrottled: typeof import('@vueuse/core')['refThrottled']
const refWithControl: typeof import('@vueuse/core')['refWithControl']
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveDirective: typeof import('vue')['resolveDirective']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const search: typeof import('./composables/settings')['search']
const sessionHost: typeof import('./composables/storage')['sessionHost']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const setTitle: typeof import('./composables/title')['setTitle']
@@ -144,10 +147,12 @@ declare global {
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
const useArraySome: typeof import('@vueuse/core')['useArraySome']
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
@@ -235,12 +240,14 @@ declare global {
const useParallax: typeof import('@vueuse/core')['useParallax']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
@@ -311,6 +318,11 @@ declare global {
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: typeof import('@vueuse/core')['whenever']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue'
}
// for vue template auto import
import { UnwrapRef } from 'vue'
declare module 'vue' {
@@ -417,10 +429,10 @@ declare module 'vue' {
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveDirective: UnwrapRef<typeof import('vue')['resolveDirective']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly search: UnwrapRef<typeof import('./composables/settings')['search']>
readonly sessionHost: UnwrapRef<typeof import('./composables/storage')['sessionHost']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly setTitle: UnwrapRef<typeof import('./composables/title')['setTitle']>
@@ -458,10 +470,12 @@ declare module 'vue' {
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
@@ -549,12 +563,14 @@ declare module 'vue' {
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
+5 -2
View File
@@ -1,5 +1,7 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
@@ -22,6 +24,7 @@ declare module '@vue/runtime-core' {
LogContainer: typeof import('./components/LogViewer/LogContainer.vue')['default']
LogDate: typeof import('./components/LogViewer/LogDate.vue')['default']
LogEventSource: typeof import('./components/LogViewer/LogEventSource.vue')['default']
LogLevel: typeof import('./components/LogViewer/LogLevel.vue')['default']
LogViewer: typeof import('./components/LogViewer/LogViewer.vue')['default']
LogViewerWithSource: typeof import('./components/LogViewer/LogViewerWithSource.vue')['default']
MdiDotsVertical: typeof import('~icons/mdi/dots-vertical')['default']
+2 -2
View File
@@ -109,8 +109,8 @@ function addColumn(container: Container) {
.panel {
min-height: 200px;
width: auto;
margin-left: 0.25rem!important;
margin-right: 0.25rem!important;
margin-left: 0.25rem !important;
margin-right: 0.25rem !important;
}
}
@@ -1,8 +1,11 @@
<template>
<div class="columns is-1 is-variable">
<div class="columns is-1 is-variable is-mobile">
<div class="column is-narrow" v-if="showTimestamp">
<log-date :date="logEntry.date"></log-date>
</div>
<div class="column is-narrow is-flex">
<log-level :level="logEntry.level"></log-level>
</div>
<div class="column">
<ul class="fields" :class="{ expanded }" @click="expanded = !expanded">
<li v-for="(value, name) in validValues(logEntry.message)">
@@ -49,12 +49,6 @@ const memoryData = computedWithControl(
</script>
<style lang="scss" scoped>
.has-spacer {
&::after {
content: " ";
}
}
.has-border {
border: 1px solid var(--primary-color);
border-radius: 3px;
@@ -12,7 +12,7 @@
</div>
</div>
</a>
<a class="dropdown-item" :href="`${base}/api/logs/download?id=${container.id}`">
<a class="dropdown-item" :href="`${base}/api/logs/download?id=${container.id}&host=${sessionHost}`">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
@@ -48,7 +48,7 @@ describe("<LogEventSource />", () => {
) {
settings.value.hourStyle = hourStyle;
search.searchFilter.value = searchFilter;
if(searchFilter){
if (searchFilter) {
search.showSearch.value = true;
}
@@ -91,22 +91,22 @@ describe("<LogEventSource />", () => {
test("should connect to EventSource", async () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
expect(sources["/api/logs/stream?id=abc&lastEventId="].readyState).toBe(1);
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen();
expect(sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].readyState).toBe(1);
wrapper.unmount();
});
test("should close EventSource", async () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen();
wrapper.unmount();
expect(sources["/api/logs/stream?id=abc&lastEventId="].readyState).toBe(2);
expect(sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].readyState).toBe(2);
});
test("should parse messages", async () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({
data: `{"ts":1560336942459, "m":"This is a message.", "id":1}`,
});
@@ -121,8 +121,8 @@ describe("<LogEventSource />", () => {
describe("render html correctly", () => {
test("should render messages", async () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({
data: `{"ts":1560336942459, "m":"This is a message.", "id":1}`,
});
@@ -134,8 +134,8 @@ describe("<LogEventSource />", () => {
test("should render messages with color", async () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({
data: '{"ts":1560336942459,"m":"\\u001b[30mblack\\u001b[37mwhite", "id":1}',
});
@@ -147,8 +147,8 @@ describe("<LogEventSource />", () => {
test("should render messages with html entities", async () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
});
@@ -160,8 +160,8 @@ describe("<LogEventSource />", () => {
test("should render dates with 12 hour style", async () => {
const wrapper = createLogEventSource({ hourStyle: "12" });
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
});
@@ -173,8 +173,8 @@ describe("<LogEventSource />", () => {
test("should render dates with 24 hour style", async () => {
const wrapper = createLogEventSource({ hourStyle: "24" });
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
});
@@ -186,11 +186,11 @@ describe("<LogEventSource />", () => {
test("should render messages with filter", async () => {
const wrapper = createLogEventSource({ searchFilter: "test" });
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({
data: `{"ts":1560336942459, "m":"foo bar", "id":1}`,
});
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({
data: `{"ts":1560336942459, "m":"test bar", "id":2}`,
});
+62
View File
@@ -0,0 +1,62 @@
<template>
<div :class="level" :data-position="position"></div>
</template>
<script lang="ts" setup>
import { Position } from "@/models/LogEntry";
defineProps<{
level?: string;
position?: Position;
}>();
</script>
<style lang="scss" scoped>
div {
display: inline-block;
width: 0.7em;
height: 0.7em;
border-radius: 0.5em;
align-self: auto;
margin-top: 0.4em;
&[data-position="start"] {
border-radius: 0.5em 0.5em 0 0;
height: 70%;
margin-bottom: -0.2em;
margin-top: auto;
align-self: flex-end;
}
&[data-position="middle"] {
border-radius: 0;
height: auto;
margin: -0.2em 0;
align-self: auto;
}
&[data-position="end"] {
border-radius: 0 0 0.5em 0.5em;
height: 70%;
margin-top: -0.2em;
align-self: flex-start;
}
&.debug,
&.trace {
background-color: #9c27b0;
}
&.info {
background-color: #00b5ad;
}
&.error,
&.fatal {
background-color: #f44336;
}
&.warn {
background-color: #ff9800;
}
}
</style>
@@ -1,8 +1,11 @@
<template>
<div class="columns is-1 is-variable">
<div class="columns is-1 is-variable is-mobile">
<div class="column is-narrow" v-if="showTimestamp">
<log-date :date="logEntry.date"></log-date>
</div>
<div class="column is-narrow is-flex">
<log-level :level="logEntry.level" :position="logEntry.position"></log-level>
</div>
<div class="text column" v-html="colorize(logEntry.message)"></div>
</div>
</template>
+1 -7
View File
@@ -4,7 +4,7 @@
<stat-sparkline :data="data" @selected-point="onSelectedPoint"></stat-sparkline>
</div>
<div class="has-background-body-color is-top-left">
<span class="has-text-weight-light has-spacer">{{ label }}</span>
<span class="has-text-weight-light">{{ label }}</span>
<span class="has-text-weight-bold">
{{ mouseOver ? selectedPoint?.value ?? selectedPoint?.y ?? statValue : statValue }}
</span>
@@ -25,12 +25,6 @@ let mouseOver = $ref(false);
</script>
<style lang="scss" scoped>
.has-spacer {
&::after {
content: " ";
}
}
.has-border {
border: 1px solid var(--primary-color);
border-radius: 3px;
@@ -1,200 +1,231 @@
// Vitest Snapshot v1
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<LogEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
<div class=\\"text column\\" data-v-a49e52d4=\\"\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render dates with 24 hour style 1`] = `
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42</time></div>
<div class=\\"text column\\" data-v-a49e52d4=\\"\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages 1`] = `
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
<div class=\\"text column\\" data-v-a49e52d4=\\"\\">This is a message.</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\">This is a message.</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with color 1`] = `
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
<div class=\\"text column\\" data-v-a49e52d4=\\"\\"><span style=\\"color:#000\\">black<span style=\\"color:#AAA\\">white</span></span></div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\"><span style=\\"color:#000\\">black<span style=\\"color:#AAA\\">white</span></span></div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with filter 1`] = `
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
<li data-key=\\"2\\" class=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"line-options\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2\\" data-v-2e92daca=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"2\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#2\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
<div class=\\"text column\\" data-v-a49e52d4=\\"\\"><mark>test</mark> bar</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\"><mark>test</mark> bar</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with html entities 1`] = `
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
"<ul data-v-2e92daca=\\"\\" class=\\"events medium\\">
<li data-v-2e92daca=\\"\\" data-key=\\"1\\" class=\\"\\">
<div data-v-2e92daca=\\"\\" class=\\"line-options\\" style=\\"display: none;\\">
<div data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\" class=\\"dropdown is-hoverable is-last is-top minimal\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-trigger\\"><button data-v-539164cb=\\"\\" class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\"><span data-v-539164cb=\\"\\" class=\\"icon\\"><svg data-v-539164cb=\\"\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div data-v-539164cb=\\"\\" class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\">
<div data-v-539164cb=\\"\\" class=\\"dropdown-content\\"><a data-v-2e92daca=\\"\\" class=\\"dropdown-item\\" href=\\"#1\\">
<div data-v-2e92daca=\\"\\" class=\\"level is-justify-content-start\\">
<div data-v-2e92daca=\\"\\" class=\\"level-left\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\"><svg data-v-2e92daca=\\"\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
<div data-v-2e92daca=\\"\\" class=\\"level-right\\">
<div data-v-2e92daca=\\"\\" class=\\"level-item\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
<div class=\\"text column\\" data-v-a49e52d4=\\"\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow is-flex\\">
<div data-v-e625cddd=\\"\\" data-v-a49e52d4=\\"\\" class=\\"\\"></div>
</div>
<div data-v-a49e52d4=\\"\\" class=\\"text column\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > renders correctly 1`] = `
"<div class=\\"infinte-loader\\" data-v-1cd63c6e=\\"\\">
<div class=\\"spinner\\" data-v-1cd63c6e=\\"\\" style=\\"display: none;\\">
<div class=\\"bounce1\\" data-v-1cd63c6e=\\"\\"></div>
<div class=\\"bounce2\\" data-v-1cd63c6e=\\"\\"></div>
<div class=\\"bounce3\\" data-v-1cd63c6e=\\"\\"></div>
"<div data-v-1cd63c6e=\\"\\" class=\\"infinte-loader\\">
<div data-v-1cd63c6e=\\"\\" class=\\"spinner\\" style=\\"display: none;\\">
<div data-v-1cd63c6e=\\"\\" class=\\"bounce1\\"></div>
<div data-v-1cd63c6e=\\"\\" class=\\"bounce2\\"></div>
<div data-v-1cd63c6e=\\"\\" class=\\"bounce3\\"></div>
</div>
</div>
<ul class=\\"events medium\\" data-v-2e92daca=\\"\\"></ul>"
<ul data-v-2e92daca=\\"\\" class=\\"events medium\\"></ul>"
`;
exports[`<LogEventSource /> > should parse messages 1`] = `
@@ -202,5 +233,7 @@ SimpleLogEntry {
"_message": "This is a message.",
"date": 2019-06-12T10:55:42.459Z,
"id": 1,
"level": undefined,
"position": undefined,
}
`;
+7 -7
View File
@@ -1,9 +1,12 @@
<template>
<time :datetime="date.toISOString()">{{ format(date) }}</time>
<div>
<time :datetime="date.toISOString()" class="is-hidden-mobile">{{ dateStr }}</time>
<time :datetime="date.toISOString()">{{ timeStr }}</time>
</div>
</template>
<script lang="ts" setup>
defineProps<{
const props = defineProps<{
date: Date;
}>();
@@ -13,9 +16,6 @@ const timeFormatter = $computed(
() => new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit", second: "2-digit", hour12: use12Hour })
);
function format(date: Date) {
const dateStr = dateFormatter.format(date);
const timeStr = timeFormatter.format(date);
return `${dateStr} ${timeStr}`;
}
const dateStr = $computed(() => dateFormatter.format(props.date));
const timeStr = $computed(() => timeFormatter.format(props.date));
</script>
+31 -7
View File
@@ -2,11 +2,33 @@
<aside>
<div class="columns is-marginless">
<div class="column is-paddingless">
<router-link :to="{ name: 'index' }">
<svg class="logo">
<use href="#logo"></use>
</svg>
</router-link>
<h1>
<router-link :to="{ name: 'index' }">
<svg class="logo">
<use href="#logo"></use>
</svg>
</router-link>
<small class="subtitle is-6 is-block mb-4" v-if="hostname">
{{ hostname }}
</small>
</h1>
<div v-if="config.hosts.length > 1" class="mb-3">
<o-dropdown v-model="sessionHost" aria-role="list">
<template #trigger>
<o-button variant="primary" type="button" size="small">
<span>{{ sessionHost }}</span>
<span class="icon">
<carbon-caret-down />
</span>
</o-button>
</template>
<o-dropdown-item :value="value" aria-role="listitem" v-for="value in config.hosts" :key="value">
<span>{{ value }}</span>
</o-dropdown-item>
</o-dropdown>
</div>
</div>
</div>
<div class="columns is-marginless">
@@ -65,9 +87,10 @@
</template>
<script lang="ts" setup>
import type { Container } from "@/types/Container";
import { Container } from "@/models/Container";
import { sessionHost } from "@/composables/storage";
const { base, secured } = config;
const { base, secured, hostname } = config;
const store = useContainerStore();
const { activeContainers, visibleContainers, ready } = storeToRefs(store);
@@ -117,6 +140,7 @@ li.exited a {
&:hover .column-icon {
visibility: visible;
&:hover {
color: var(--secondary-color);
}
+3 -1
View File
@@ -58,7 +58,9 @@ export function useLogStream(container: ComputedRef<Container>) {
lastEventId = "";
}
es = new EventSource(`${config.base}/api/logs/stream?id=${container.value.id}&lastEventId=${lastEventId}`);
es = new EventSource(
`${config.base}/api/logs/stream?id=${container.value.id}&lastEventId=${lastEventId}&host=${sessionHost.value}`
);
es.addEventListener("container-stopped", () => {
es?.close();
es = null;
+1 -1
View File
@@ -87,5 +87,5 @@ export {
menuWidth,
size,
search,
settings
settings,
};
+3
View File
@@ -0,0 +1,3 @@
const sessionHost = useSessionStorage("host", "localhost");
export { sessionHost };
+2 -1
View File
@@ -1,5 +1,6 @@
const { hostname } = config;
let subtitle = $ref("");
const title = $computed(() => `${subtitle} - Dozzle`);
const title = $computed(() => `${subtitle} - Dozzle` + (hostname ? ` @ ${hostname}` : ""));
useTitle($$(title));
+26 -9
View File
@@ -11,16 +11,18 @@ export interface HasComponent {
export type JSONValue = string | number | boolean | JSONObject | Array<JSONValue>;
export type JSONObject = { [x: string]: JSONValue };
export type Position = "start" | "end" | "middle" | undefined;
export interface LogEvent {
readonly m: string | JSONObject;
readonly ts: number;
readonly id: number;
readonly l: string;
readonly p: Position;
}
export abstract class LogEntry<T extends string | JSONObject> implements HasComponent {
protected readonly _message: T;
constructor(message: T, public readonly id: number, public readonly date: Date) {
constructor(message: T, public readonly id: number, public readonly date: Date, public readonly level?: string) {
this._message = message;
}
@@ -32,6 +34,15 @@ export abstract class LogEntry<T extends string | JSONObject> implements HasComp
}
export class SimpleLogEntry extends LogEntry<string> {
constructor(
message: string,
id: number,
date: Date,
public readonly level: string,
public readonly position: Position
) {
super(message, id, date, level);
}
getComponent(): Component {
return SimpleLogItem;
}
@@ -40,8 +51,14 @@ export class SimpleLogEntry extends LogEntry<string> {
export class ComplexLogEntry extends LogEntry<JSONObject> {
private readonly filteredMessage: ComputedRef<JSONObject>;
constructor(message: JSONObject, id: number, date: Date, visibleKeys?: Ref<string[][]>) {
super(message, id, date);
constructor(
message: JSONObject,
id: number,
date: Date,
public readonly level: string,
visibleKeys?: Ref<string[][]>
) {
super(message, id, date, level);
if (visibleKeys) {
this.filteredMessage = computed(() => {
if (!visibleKeys.value.length) {
@@ -67,13 +84,13 @@ export class ComplexLogEntry extends LogEntry<JSONObject> {
}
static fromLogEvent(event: ComplexLogEntry, visibleKeys: Ref<string[][]>): ComplexLogEntry {
return new ComplexLogEntry(event._message, event.id, event.date, visibleKeys);
return new ComplexLogEntry(event._message, event.id, event.date, event.level, visibleKeys);
}
}
export class DockerEventLogEntry extends LogEntry<string> {
constructor(message: string, date: Date, public readonly event: string) {
super(message, date.getTime(), date);
super(message, date.getTime(), date, "info");
}
getComponent(): Component {
return DockerEventLogItem;
@@ -90,7 +107,7 @@ export class SkippedLogsEntry extends LogEntry<string> {
public readonly firstSkipped: LogEntry<string | JSONObject>,
lastSkipped: LogEntry<string | JSONObject>
) {
super("", date.getTime(), date);
super("", date.getTime(), date, "info");
this._totalSkipped = totalSkipped;
this.lastSkipped = lastSkipped;
}
@@ -114,8 +131,8 @@ export class SkippedLogsEntry extends LogEntry<string> {
export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
if (typeof event.m === "string") {
return new SimpleLogEntry(event.m, event.id, new Date(event.ts));
return new SimpleLogEntry(event.m, event.id, new Date(event.ts), event.l, event.p);
} else {
return new ComplexLogEntry(event.m, event.id, new Date(event.ts));
return new ComplexLogEntry(event.m, event.id, new Date(event.ts), event.l);
}
}
+1 -2
View File
@@ -1,8 +1,7 @@
import { type App } from "vue";
import { createPinia } from "pinia";
export const install = (app:App) => {
export const install = (app: App) => {
const pinia = createPinia();
app.use(pinia);
};
+14 -2
View File
@@ -78,7 +78,7 @@
import SearchIcon from "~icons/mdi-light/magnify";
import { useFuse } from "@vueuse/integrations/useFuse";
const { base, version, secured } = config;
const { version } = config;
const containerStore = useContainerStore();
const { containers } = storeToRefs(containerStore);
const router = useRouter();
@@ -89,10 +89,22 @@ const query = ref("");
const mostRecentContainers = $computed(() => [...containers.value].sort((a, b) => b.created - a.created));
const runningContainers = $computed(() => mostRecentContainers.filter((c) => c.state === "running"));
const { results } = useFuse(query, containers, {
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);
+5
View File
@@ -6,6 +6,8 @@ interface Config {
authorizationNeeded: boolean | "false" | "true";
secured: boolean | "false" | "true";
maxLogs: number;
hostname: string;
hosts: string[] | string;
}
const pageConfig = JSON.parse(text);
@@ -20,10 +22,13 @@ if (config.version == "{{ .Version }}") {
config.base = "";
config.authorizationNeeded = false;
config.secured = false;
config.hostname = "localhost";
config.hosts = ["localhost"];
} else {
config.version = config.version.replace(/^v/, "");
config.authorizationNeeded = config.authorizationNeeded === "true";
config.secured = config.secured === "true";
config.hosts = (config.hosts as string).split(",");
}
export default config as Config;
+35 -21
View File
@@ -6,6 +6,8 @@ import { Container } from "@/models/Container";
export const useContainerStore = defineStore("container", () => {
const containers: Ref<Container[]> = ref([]);
const activeContainerIds: Ref<string[]> = ref([]);
let es: EventSource | null = null;
const ready = ref(false);
const allContainersById = computed(() =>
containers.value.reduce((acc, container) => {
@@ -21,25 +23,40 @@ export const useContainerStore = defineStore("container", () => {
const activeContainers = computed(() => activeContainerIds.value.map((id) => allContainersById.value[id]));
const es = new EventSource(`${config.base}/api/events/stream`);
es.addEventListener("containers-changed", (e: Event) =>
setContainers(JSON.parse((e as MessageEvent).data) as ContainerJson[])
watch(
sessionHost,
() => {
connect();
},
{ immediate: true }
);
es.addEventListener("container-stat", (e) => {
const stat = JSON.parse((e as MessageEvent).data) as ContainerStat;
const container = allContainersById.value[stat.id] as unknown as UnwrapNestedRefs<Container>;
if (container) {
const { id, ...rest } = stat;
container.stat = rest;
}
});
es.addEventListener("container-die", (e) => {
const event = JSON.parse((e as MessageEvent).data) as { actorId: string };
const container = allContainersById.value[event.actorId];
if (container) {
container.state = "dead";
}
});
function connect() {
es?.close();
ready.value = false;
es = new EventSource(`${config.base}/api/events/stream?host=${sessionHost.value}`);
es.addEventListener("containers-changed", (e: Event) =>
setContainers(JSON.parse((e as MessageEvent).data) as ContainerJson[])
);
es.addEventListener("container-stat", (e) => {
const stat = JSON.parse((e as MessageEvent).data) as ContainerStat;
const container = allContainersById.value[stat.id] as unknown as UnwrapNestedRefs<Container>;
if (container) {
const { id, ...rest } = stat;
container.stat = rest;
}
});
es.addEventListener("container-die", (e) => {
const event = JSON.parse((e as MessageEvent).data) as { actorId: string };
const container = allContainersById.value[event.actorId];
if (container) {
container.state = "dead";
}
});
watchOnce(containers, () => (ready.value = true));
}
const setContainers = (newContainers: ContainerJson[]) => {
containers.value = newContainers.map((c) => {
@@ -58,9 +75,6 @@ export const useContainerStore = defineStore("container", () => {
const removeActiveContainer = ({ id }: Container) =>
activeContainerIds.value.splice(activeContainerIds.value.indexOf(id), 1);
const ready = ref(false);
watchOnce(containers, () => (ready.value = true));
return {
containers,
activeContainerIds,
+3
View File
@@ -177,6 +177,9 @@ html.has-custom-scrollbars {
.modal {
z-index: 1000;
.modal-background {
backdrop-filter: blur(4px);
}
}
.button .button-wrapper > span {
+59 -3
View File
@@ -5,6 +5,9 @@ import (
"encoding/json"
"fmt"
"io"
"net/url"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
@@ -14,6 +17,7 @@ import (
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
log "github.com/sirupsen/logrus"
)
@@ -35,7 +39,7 @@ type dockerProxy interface {
type Client interface {
ListContainers() ([]Container, error)
FindContainer(string) (Container, error)
ContainerLogs(context.Context, string, int, string) (io.ReadCloser, error)
ContainerLogs(context.Context, string, string) (io.ReadCloser, error)
Events(context.Context) (<-chan ContainerEvent, <-chan error)
ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time) (io.ReadCloser, error)
ContainerStats(context.Context, string, chan<- ContainerStat) error
@@ -62,6 +66,58 @@ func NewClientWithFilters(f map[string][]string) Client {
return &dockerClient{cli, filterArgs}
}
func NewClientWithTlsAndFilter(f map[string][]string, connection string) Client {
filterArgs := filters.NewArgs()
for key, values := range f {
for _, value := range values {
filterArgs.Add(key, value)
}
}
log.Debugf("filterArgs = %v", filterArgs)
remoteUrl, err := url.Parse(connection)
if err != nil {
log.Fatal(err)
}
if remoteUrl.Scheme != "tcp" {
log.Fatal("Only tcp scheme is supported")
}
host := remoteUrl.Hostname()
basePath := "/certs"
if _, err := os.Stat(filepath.Join(basePath, host)); !os.IsNotExist(err) {
basePath = filepath.Join(basePath, host)
}
cacertPath := filepath.Join(basePath, "ca.pem")
certPath := filepath.Join(basePath, "cert.pem")
keyPath := filepath.Join(basePath, "key.pem")
opts := []client.Opt{
client.WithHost(connection),
}
if _, err := os.Stat(cacertPath); os.IsNotExist(err) {
log.Debugf("%s does not exist, using plain HTTP", cacertPath)
} else {
log.Debugf("Using TLS client config with certs at: %s", basePath)
opts = append(opts, client.WithTLSClientConfig(cacertPath, certPath, keyPath))
}
opts = append(opts, client.WithAPIVersionNegotiation())
cli, err := client.NewClientWithOpts(opts...)
if err != nil {
log.Fatal(err)
}
return &dockerClient{cli, filterArgs}
}
func (d *dockerClient) FindContainer(id string) (Container, error) {
var container Container
containers, err := d.ListContainers()
@@ -169,7 +225,7 @@ func (d *dockerClient) ContainerStats(ctx context.Context, id string, stats chan
return nil
}
func (d *dockerClient) ContainerLogs(ctx context.Context, id string, tailSize int, since string) (io.ReadCloser, error) {
func (d *dockerClient) ContainerLogs(ctx context.Context, id string, since string) (io.ReadCloser, error) {
log.WithField("id", id).WithField("since", since).Debug("streaming logs for container")
if since != "" {
@@ -184,7 +240,7 @@ func (d *dockerClient) ContainerLogs(ctx context.Context, id string, tailSize in
ShowStdout: true,
ShowStderr: true,
Follow: true,
Tail: strconv.Itoa(tailSize),
Tail: "300",
Timestamps: true,
Since: since,
}
+8 -8
View File
@@ -6,7 +6,7 @@ import (
"encoding/binary"
"errors"
"io"
"io/ioutil"
"strings"
"testing"
@@ -125,7 +125,7 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) {
binary.BigEndian.PutUint32(b[4:], uint32(len(expected)))
b = append(b, []byte(expected)...)
reader := ioutil.NopCloser(bytes.NewReader(b))
reader := io.NopCloser(bytes.NewReader(b))
options := types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: "300", Timestamps: true, Since: "since"}
proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil)
@@ -133,9 +133,9 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) {
proxy.On("ContainerInspect", mock.Anything, id).Return(json, nil)
client := &dockerClient{proxy, filters.NewArgs()}
logReader, _ := client.ContainerLogs(context.Background(), id, 300, "since")
logReader, _ := client.ContainerLogs(context.Background(), id, "since")
actual, _ := ioutil.ReadAll(logReader)
actual, _ := io.ReadAll(logReader)
assert.Equal(t, expected, string(actual), "message doesn't match expected")
proxy.AssertExpectations(t)
}
@@ -146,7 +146,7 @@ func Test_dockerClient_ContainerLogs_happy_with_tty(t *testing.T) {
proxy := new(mockedProxy)
expected := "INFO Testing logs..."
reader := ioutil.NopCloser(strings.NewReader(expected))
reader := io.NopCloser(strings.NewReader(expected))
options := types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: "300", Timestamps: true}
proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil)
@@ -154,9 +154,9 @@ func Test_dockerClient_ContainerLogs_happy_with_tty(t *testing.T) {
proxy.On("ContainerInspect", mock.Anything, id).Return(json, nil)
client := &dockerClient{proxy, filters.NewArgs()}
logReader, _ := client.ContainerLogs(context.Background(), id, 300, "")
logReader, _ := client.ContainerLogs(context.Background(), id, "")
actual, _ := ioutil.ReadAll(logReader)
actual, _ := io.ReadAll(logReader)
assert.Equal(t, expected, string(actual), "message doesn't match expected")
proxy.AssertExpectations(t)
@@ -170,7 +170,7 @@ func Test_dockerClient_ContainerLogs_error(t *testing.T) {
client := &dockerClient{proxy, filters.NewArgs()}
reader, err := client.ContainerLogs(context.Background(), id, 300, "")
reader, err := client.ContainerLogs(context.Background(), id, "")
assert.Nil(t, reader, "reader should be nil")
assert.Error(t, err, "error should have been returned")
+156
View File
@@ -0,0 +1,156 @@
package docker
import (
"bufio"
"encoding/json"
"hash/fnv"
"regexp"
"strings"
"time"
log "github.com/sirupsen/logrus"
)
type eventGenerator struct {
reader *bufio.Reader
channel chan *LogEvent
next *LogEvent
lastError error
}
func NewEventIterator(reader *bufio.Reader) *eventGenerator {
generator := &eventGenerator{reader: reader, channel: make(chan *LogEvent, 100)}
go generator.consume()
return generator
}
func (g *eventGenerator) Next() (*LogEvent, error) {
var currentEvent *LogEvent
var nextEvent *LogEvent
if g.next != nil {
currentEvent = g.next
g.next = nil
nextEvent = g.Peek()
} else {
event, ok := <-g.channel
if !ok {
return nil, g.lastError
}
currentEvent = event
nextEvent = g.Peek()
}
currentLevel := guessLogLevel(currentEvent)
if nextEvent != nil {
if currentEvent.IsCloseToTime(nextEvent) && currentLevel != "" && !nextEvent.HasLevel() {
currentEvent.Position = START
nextEvent.Position = MIDDLE
}
// If next item is not close to current item or has level, set current item position to end
if currentEvent.Position == MIDDLE && (nextEvent.HasLevel() || !currentEvent.IsCloseToTime(nextEvent)) {
currentEvent.Position = END
}
// If next item is close to current item and has no level, set next item position to middle
if currentEvent.Position == MIDDLE && !nextEvent.HasLevel() && currentEvent.IsCloseToTime(nextEvent) {
nextEvent.Position = MIDDLE
}
// Set next item level to current item level
if currentEvent.Position == START || currentEvent.Position == MIDDLE {
nextEvent.Level = currentEvent.Level
}
} else if currentEvent.Position == MIDDLE {
currentEvent.Position = END
}
return currentEvent, nil
}
func (g *eventGenerator) LastError() error {
return g.lastError
}
func (g *eventGenerator) Peek() *LogEvent {
if g.next != nil {
return g.next
}
select {
case event := <-g.channel:
g.next = event
return g.next
case <-time.After(50 * time.Millisecond):
return nil
}
}
func (g *eventGenerator) consume() {
for {
message, readerError := g.reader.ReadString('\n')
if message != "" {
h := fnv.New32a()
h.Write([]byte(message))
logEvent := &LogEvent{Id: h.Sum32(), Message: message}
if index := strings.IndexAny(message, " "); index != -1 {
logId := message[:index]
if timestamp, err := time.Parse(time.RFC3339Nano, logId); err == nil {
logEvent.Timestamp = timestamp.UnixMilli()
message = strings.TrimSuffix(message[index+1:], "\n")
logEvent.Message = message
if strings.HasPrefix(message, "{") && strings.HasSuffix(message, "}") {
var data map[string]interface{}
if err := json.Unmarshal([]byte(message), &data); err != nil {
log.Errorf("json unmarshal error while streaming %v", err.Error())
} else {
logEvent.Message = data
}
}
}
}
logEvent.Level = guessLogLevel(logEvent)
g.channel <- logEvent
}
if readerError != nil {
g.lastError = readerError
close(g.channel)
return
}
}
}
var NON_ASCII_REGEX = regexp.MustCompile("^[^a-zA-z ]+[^ewidtf]?")
var KEY_VALUE_REGEX = regexp.MustCompile(`level=(\w+)`)
func guessLogLevel(logEvent *LogEvent) string {
switch value := logEvent.Message.(type) {
case string:
levels := []string{"error", "warn", "info", "debug", "trace", "fatal"}
stripped := NON_ASCII_REGEX.ReplaceAllString(value, "")
for _, level := range levels {
if match, _ := regexp.MatchString("(?i)^"+level+"[^a-z]", stripped); match {
return level
}
if strings.Contains(value, " "+strings.ToUpper(level)+" ") {
return level
}
}
if matches := KEY_VALUE_REGEX.FindStringSubmatch(value); matches != nil {
return matches[1]
}
case map[string]interface{}:
if level, ok := value["level"].(string); ok {
return level
}
}
return ""
}
+56
View File
@@ -0,0 +1,56 @@
package docker
import (
"bufio"
"io"
"strings"
"testing"
"github.com/magiconair/properties/assert"
"github.com/stretchr/testify/require"
)
func TestNewEventIterator(t *testing.T) {
input := "example input"
reader := bufio.NewReader(strings.NewReader(input))
generator := NewEventIterator(reader)
require.NotNil(t, generator, "Expected generator to not be nil, but got nil")
}
func TestEventGenerator_Next(t *testing.T) {
input := "example input"
reader := bufio.NewReader(strings.NewReader(input))
generator := NewEventIterator(reader)
event, err := generator.Next()
require.NoError(t, err, "Expected no error, but got: %v", err)
require.NotNil(t, event, "Expected event to not be nil, but got nil")
}
func TestEventGenerator_LastError(t *testing.T) {
input := "example input"
reader := bufio.NewReader(strings.NewReader(input))
generator := NewEventIterator(reader)
require.Nil(t, generator.LastError(), "Expected LastError to return nil, but got: %v", generator.LastError())
generator.Next()
// expert error to be EOF
assert.Equal(t, generator.LastError(), io.EOF, "Expected LastError to return EOF, but got: %v", generator.LastError().Error())
}
func TestEventGenerator_Peek(t *testing.T) {
input := "example input"
reader := bufio.NewReader(strings.NewReader(input))
generator := NewEventIterator(reader)
event := generator.Peek()
require.NotNil(t, event, "Expected event to not be nil, but got nil")
assert.Equal(t, event.Message, input, "Expected event message to be %s, but got: %s", input, event.Message.(string))
}
+23 -3
View File
@@ -1,5 +1,7 @@
package docker
import "math"
// Container represents an internal representation of docker containers
type Container struct {
ID string `json:"id"`
@@ -27,8 +29,26 @@ type ContainerEvent struct {
Name string `json:"name"`
}
type LogPosition string
const (
START LogPosition = "start"
MIDDLE LogPosition = "middle"
END LogPosition = "end"
)
type LogEvent struct {
Message any `json:"m,omitempty"`
Timestamp int64 `json:"ts"`
Id uint32 `json:"id,omitempty"`
Message any `json:"m,omitempty"`
Timestamp int64 `json:"ts"`
Id uint32 `json:"id,omitempty"`
Level string `json:"l,omitempty"`
Position LogPosition `json:"p,omitempty"`
}
func (l *LogEvent) HasLevel() bool {
return l.Level != ""
}
func (l *LogEvent) IsCloseToTime(other *LogEvent) bool {
return math.Abs(float64(l.Timestamp-other.Timestamp)) < 10
}
+10
View File
@@ -0,0 +1,10 @@
.DS_Store
.vite-ssg-dist
.vite-ssg-temp
*.local
dist
dist-ssr
node_modules
.idea/
*.log
!.vscode
+2
View File
@@ -0,0 +1,2 @@
dist
cache
+117
View File
@@ -0,0 +1,117 @@
import { createRequire } from "module";
import { defineConfig } from "vitepress";
import { createWriteStream } from "node:fs";
import { resolve } from "node:path";
import { SitemapStream, streamToPromise } from "sitemap";
const require = createRequire(import.meta.url);
const pkg = require("dozzle/package.json");
const links = [] as { url: string; lastmod?: number }[];
export default defineConfig({
lang: "en-US",
title: "Dozzle",
description: "A lightweight, open-source, and secure log viewer for Docker.",
lastUpdated: true,
cleanUrls: true,
head: [
[
"script",
{
async: "",
src: "https://www.googletagmanager.com/gtag/js?id=G-X3Z4496XFK",
},
],
[
"script",
{},
`function gtag(){dataLayer.push(arguments)}window.dataLayer=window.dataLayer||[],gtag("js",new Date),gtag("config","G-X3Z4496XFK")`,
],
],
themeConfig: {
editLink: {
pattern: "https://github.com/amir20/dozzle/edit/master/docs/:path",
},
nav: [
{ text: "Home", link: "/" },
{ text: "Guide", link: "/guide/what-is-dozzle", activeMatch: "/guide/" },
{
text: `v${pkg.version}`,
items: [
{
text: "Releases",
link: "https://github.com/amir20/dozzle/releases",
},
{
text: "New Issue",
link: "https://github.com/amir20/dozzle/issues/new/choose",
},
],
},
],
sidebar: [
{
text: "Introduction",
items: [
{ text: "What is Dozzle?", link: "/guide/what-is-dozzle" },
{ text: "Getting Started", link: "/guide/getting-started" },
],
},
{
text: "Advanced Configuration",
items: [
{ text: "Authentication", link: "/guide/authentication" },
{ text: "Healthcheck", link: "/guide/healthcheck" },
{ text: "Remote Hosts", link: "/guide/remote-hosts" },
{ text: "Supported Env Vars", link: "/guide/supported-env-vars" },
],
},
{
text: "Troubleshooting",
items: [
{ text: "FAQ", link: "/guide/faq" },
{ text: "Debugging", link: "/guide/debugging" },
],
},
],
footer: {
message: "Released under the MIT License. Open sourced and sponsored by Docker OSS.",
copyright: "Copyright © 2019-present <a href='https://amirraminfar.me'>Amir Raminfar</a>",
},
socialLinks: [
{ icon: "github", link: "https://github.com/amir20/dozzle" },
{
icon: {
svg: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M21.81 10.25c-.06-.04-.56-.43-1.64-.43c-.28 0-.56.03-.84.08c-.21-1.4-1.38-2.11-1.43-2.14l-.29-.17l-.18.27c-.24.36-.43.77-.51 1.19c-.2.8-.08 1.56.33 2.21c-.49.28-1.29.35-1.46.35H2.62c-.34 0-.62.28-.62.63c0 1.15.18 2.3.58 3.38c.45 1.19 1.13 2.07 2 2.61c.98.6 2.59.94 4.42.94c.79 0 1.61-.07 2.42-.22c1.12-.2 2.2-.59 3.19-1.16A8.3 8.3 0 0 0 16.78 16c1.05-1.17 1.67-2.5 2.12-3.65h.19c1.14 0 1.85-.46 2.24-.85c.26-.24.45-.53.59-.87l.08-.24l-.19-.14m-17.96.99h1.76c.08 0 .16-.07.16-.16V9.5c0-.08-.07-.16-.16-.16H3.85c-.09 0-.16.07-.16.16v1.58c.01.09.07.16.16.16m2.43 0h1.76c.08 0 .16-.07.16-.16V9.5c0-.08-.07-.16-.16-.16H6.28c-.09 0-.16.07-.16.16v1.58c.01.09.07.16.16.16m2.47 0h1.75c.1 0 .17-.07.17-.16V9.5c0-.08-.06-.16-.17-.16H8.75c-.08 0-.15.07-.15.16v1.58c0 .09.06.16.15.16m2.44 0h1.77c.08 0 .15-.07.15-.16V9.5c0-.08-.06-.16-.15-.16h-1.77c-.08 0-.15.07-.15.16v1.58c0 .09.07.16.15.16M6.28 9h1.76c.08 0 .16-.09.16-.18V7.25c0-.09-.07-.16-.16-.16H6.28c-.09 0-.16.06-.16.16v1.57c.01.09.07.18.16.18m2.47 0h1.75c.1 0 .17-.09.17-.18V7.25c0-.09-.06-.16-.17-.16H8.75c-.08 0-.15.06-.15.16v1.57c0 .09.06.18.15.18m2.44 0h1.77c.08 0 .15-.09.15-.18V7.25c0-.09-.07-.16-.15-.16h-1.77c-.08 0-.15.06-.15.16v1.57c0 .09.07.18.15.18m0-2.28h1.77c.08 0 .15-.07.15-.16V5c0-.1-.07-.17-.15-.17h-1.77c-.08 0-.15.06-.15.17v1.56c0 .08.07.16.15.16m2.46 4.52h1.76c.09 0 .16-.07.16-.16V9.5c0-.08-.07-.16-.16-.16h-1.76c-.08 0-.15.07-.15.16v1.58c0 .09.07.16.15.16"/></svg>`,
},
link: "https://hub.docker.com/r/amir20/dozzle",
},
],
},
transformHtml: (_, id, { pageData }) => {
if (!/[\\/]404\.html$/.test(id))
links.push({
// you might need to change this if not using clean urls mode
url: pageData.relativePath.replace(/((^|\/)index)?\.md$/, "$2"),
lastmod: pageData.lastUpdated,
});
},
buildEnd: async ({ outDir }) => {
const sitemap = new SitemapStream({
hostname: "https://dozzle.dev/",
});
const writeStream = createWriteStream(resolve(outDir, "sitemap.xml"));
sitemap.pipe(writeStream);
links.forEach((link) => sitemap.write(link));
const promise = streamToPromise(sitemap);
sitemap.end();
await promise;
},
});
@@ -0,0 +1,14 @@
<script setup></script>
<template>
<div border="~ solid rounded light-100 dark:dark-50" bg="[#eee] dark:[#222]">
<div flex gap="2" p-2>
<div circle-red />
<div circle-yellow />
<div circle-green />
</div>
<div class="content">
<slot />
</div>
</div>
</template>
@@ -0,0 +1,37 @@
<script lang="ts" setup>
import { useMutationObserver } from "@vueuse/core";
import { onMounted, ref } from "vue";
const isDark = ref(false);
onMounted(() => {
isDark.value = document.documentElement.classList.contains("dark");
});
useMutationObserver(
document.documentElement,
(mutations) => {
isDark.value = document.documentElement.classList.contains("dark");
},
{
attributes: true,
attributeFilter: ["class"],
}
);
</script>
<template>
<browser-window drop-shadow-md>
<video muted loop autoplay playsinline poster="../media/poster.png" v-if="isDark">
<source src="../media/dozzle-dark.webm" type="video/webm" />
<source src="../media/dozzle-dark.mp4" type="video/mp4" />
<img src="../media/poster.png" alt="" />
</video>
<video muted loop autoplay playsinline poster="../media/poster.png" v-else>
<source src="../media/dozzle-light.webm" type="video/webm" />
<source src="../media/dozzle-light.mp4" type="video/mp4" />
<img src="../media/poster.png" alt="" />
</video>
</browser-window>
</template>
<style scoped></style>
+16
View File
@@ -0,0 +1,16 @@
// https://vitepress.dev/guide/custom-theme
import { h } from "vue";
import Theme from "vitepress/theme";
import "uno.css";
import "./style.css";
import HeroVideo from "./components/HeroVideo.vue";
export default {
...Theme,
Layout: () => {
return h(Theme.Layout, null, {
"home-hero-image": () => h(HeroVideo),
});
},
enhanceApp({ app, router, siteData }) {},
};
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 KiB

+157
View File
@@ -0,0 +1,157 @@
/**
* Customize default theme styling by overriding CSS variables:
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
*/
/**
* Colors
* -------------------------------------------------------------------------- */
:root {
--vp-c-brand: #646cff;
--vp-c-brand-light: #747bff;
--vp-c-brand-lighter: #9499ff;
--vp-c-brand-lightest: #bcc0ff;
--vp-c-brand-dark: #535bf2;
--vp-c-brand-darker: #454ce1;
--vp-c-brand-dimm: rgba(100, 108, 255, 0.08);
}
/**
* Component: Button
* -------------------------------------------------------------------------- */
:root {
--vp-button-brand-border: var(--vp-c-brand-light);
--vp-button-brand-text: var(--vp-c-white);
--vp-button-brand-bg: var(--vp-c-brand);
--vp-button-brand-hover-border: var(--vp-c-brand-light);
--vp-button-brand-hover-text: var(--vp-c-white);
--vp-button-brand-hover-bg: var(--vp-c-brand-light);
--vp-button-brand-active-border: var(--vp-c-brand-light);
--vp-button-brand-active-text: var(--vp-c-white);
--vp-button-brand-active-bg: var(--vp-button-brand-bg);
}
/**
* Component: Home
* -------------------------------------------------------------------------- */
:root {
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: linear-gradient(124deg, #de4daa 30%, #f6d327);
--vp-home-hero-image-background-image: linear-gradient(-45deg, hsl(44, 100%, 77%) 50%, #47caff 50%);
--vp-home-hero-image-filter: blur(40px);
}
@media (min-width: 640px) {
:root {
--vp-home-hero-image-filter: blur(56px);
}
}
@media (min-width: 960px) {
:root {
--vp-home-hero-image-filter: blur(72px);
}
}
/**
* Component: Custom Block
* -------------------------------------------------------------------------- */
:root {
--vp-custom-block-tip-border: var(--vp-c-brand);
--vp-custom-block-tip-text: var(--vp-c-brand-darker);
--vp-custom-block-tip-bg: var(--vp-c-brand-dimm);
--tag-line-color: #333;
--bar-title-color: #ef5734;
}
.dark {
--vp-custom-block-tip-border: var(--vp-c-brand);
--vp-custom-block-tip-text: var(--vp-c-brand-lightest);
--vp-custom-block-tip-bg: var(--vp-c-brand-dimm);
--tag-line-color: white;
--bar-title-color: hsl(44, 100%, 77%);
}
/**
* Component: Algolia
* -------------------------------------------------------------------------- */
.DocSearch {
--docsearch-primary-color: var(--vp-c-brand) !important;
}
/**
* VitePress: Custom fix
* -------------------------------------------------------------------------- */
/*
Use lighter colors for links in dark mode for a11y.
Also specify some classes twice to have higher specificity
over scoped class data attribute.
*/
.dark .vp-doc a,
.dark .vp-doc a > code,
.dark .VPNavBarMenuLink.VPNavBarMenuLink:hover,
.dark .VPNavBarMenuLink.VPNavBarMenuLink.active,
.dark .link.link:hover,
.dark .link.link.active,
.dark .edit-link-button.edit-link-button,
.dark .pager-link .title {
color: var(--vp-c-brand-lighter);
}
.dark .vp-doc a:hover,
.dark .vp-doc a > code:hover {
color: var(--vp-c-brand-lightest);
opacity: 1;
}
/* Transition by color instead of opacity */
.dark .vp-doc .custom-block a {
transition: color 0.25s;
}
/**
* Dozzle css
* --------------------------------------------------------------------------
**/
@media (max-width: 960px) {
.is-home .VPHero {
margin-top: -1em;
}
}
.VPHero .container {
max-width: 100%;
justify-content: center;
}
@media (min-width: 960px) {
.is-home .container .main {
padding-right: 4em;
}
}
.VPHomeHero .tagline {
@apply font-sans text-4xl font-light;
color: var(--tag-line-color);
}
.VPHomeHero .name {
@apply font-playfair font-light text-7xl;
}
.VPNavBarTitle .title {
@apply font-playfair font-light text-3xl;
color: var(--bar-title-color);
}
.VPHero .container .image {
max-width: 1200px;
}
+17
View File
@@ -0,0 +1,17 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
BrowserWindow: typeof import('./.vitepress/theme/components/BrowserWindow.vue')['default']
HeroVideo: typeof import('./.vitepress/theme/components/HeroVideo.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}
+57
View File
@@ -0,0 +1,57 @@
---
title: Authentication
---
# Setting Up Authentication
Dozzle supports a very simple authentication out of the box with just username and password. You should deploy using SSL to keep the credentials safe. See configuration to use `--username` and `--password`. You can also use docker secrets `--usernamefile` and `--passwordfile`.
::: code-group
```sh [cli]
$ docker run -v /var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir20/dozzle --username amirraminfar --password supersecretpassword
```
```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_USERNAME: amirraminfar
DOZZLE_PASSWORD: supersecretpassword
```
:::
## Setting up authentication with Docker secrets
Dozzle also support path to file for username and password which can be used to with Docker Secrets.
```yaml
version: "3"
services:
dozzle:
image: amir20/dozzle:latest
environment:
DOZZLE_LEVEL: debug
DOZZLE_USERNAME_FILE: /run/secrets/dozzle_user
DOZZLE_PASSWORD_FILE: /run/secrets/dozzle_password
volumes:
- /var/run/docker.sock:/var/run/docker.sock
secrets:
- dozzle_user
- dozzle_password
ports:
- 8080:8080
secrets:
dozzle_user:
file: dozzle_user.txt
dozzle_password:
file: dozzle_password.txt
```
+20
View File
@@ -0,0 +1,20 @@
---
title: Debugging
---
# Debugging with logs
By default, Dozzle does not output a lot of logs. However this can be changed with `--level` flag. The default value is `info` which only prints limited logs. You can use `debug` or `trace` which will show details about memory, configuration and other stats. `DOZZLE_LEVEL` can be used in compose configurations. Below is an example of using `docker-compose.yml` file to enable `debug` level.
```yaml
version: "3"
services:
dozzle:
image: amir20/dozzle:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
ports:
- 8080:8080
environment:
DOZZLE_LEVEL: debug
```
+51
View File
@@ -0,0 +1,51 @@
---
title: FAQ
---
# Frequently Asked Questions
## I installed Dozzle, but logs are slow or they never load. What do I do?
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.
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;
}
}
```
## We have tools that uses Dozzle when a new container is created. How can I get a direct link to a container by name?
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`.
## I installed Dozzle but memory consumption doesn't show up!
_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
```
+31
View File
@@ -0,0 +1,31 @@
---
title: Getting Started
---
# Getting Started
This section will help you to setup Dozzle locally. Dozzle can also be used to connect to remote hosts via `tcp://` and tls. See remote host if you want to connect to other hosts.
## Using Docker CLI
The easiest way to setup Dozzle is to use the CLI and mount `docker.sock` file. This file is usually located at `/var/run/docker.sock` and can be mounted with the `--volume` flag. You also need to expose the port to view Dozzle. By default, Dozzle listens on port 8080, but you can change the external port using `-p`.
```sh
docker run --detach --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir20/dozzle
```
## Using Docker Compose
Docker compose makes it easier to configure Dozzle as part of an existing configuration.
```yaml
version: "3"
services:
dozzle:
container_name: dozzle
image: amir20/dozzle:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
ports:
- 9999:8080
```
+24
View File
@@ -0,0 +1,24 @@
---
title: Healthcheck
---
# Adding healthcheck
Dozzle doesn't enable healthcheck by default as it adds extra CPU usage. `healthcheck` can be enabled manually.
```yaml
version: "3"
services:
dozzle:
image: amir20/dozzle:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
ports:
- 8080:8080
healthcheck:
test: ["CMD", "/dozzle", "healthcheck"]
interval: 3s
timeout: 30s
retries: 5
start_period: 30s
```
+55
View File
@@ -0,0 +1,55 @@
---
title: Remote Host Setup
---
# Remote Host Setup
Dozzle supports connecting to multiple remote hosts via `tcp://` using TLS and non-secured connections. Dozzle will need to have appropriate certs mounted to use secured connection. `ssh://` is not supported because Dozzle docker image does not ship with any ssh clients.
## Connecting to remote hosts
Remote hosts can be configured with `--remote-host` or `DOZZLE_REMOTE_HOST`. All certs must be mounted to `/certs` directory. The `/cert` directory expects to have `/certs/{ca,cert,key}.pem` or `/certs/{host}/{ca,cert,key}.pem` in case of multiple hosts.
Multiple `--remote-host` flags can be used to specify multiple hosts. However, using `DOZZLE_REMOTE_HOST` the value should be comma separated.
::: code-group
```sh [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 --remote-host tcp://167.99.1.2:2376
```
```yaml [docker-compose.yml]
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
```
:::
## Connecting with a socket proxy
If you are in a private network then you can use [Docker Socket Proxy](https://github.com/Tecnativa/docker-socket-proxy) which expose `docker.sock` file without the need of TLS. Dozzle will never try to write to Docker but it will need access to list APIs. The following command will start a proxy with minimal access.
```sh
docker container run --privileged -e CONTAINERS=1 -v /var/run/docker.sock:/var/run/docker.sock -p 2375:2375 tecnativa/docker-socket-proxy
```
Note that `CONTAINERS=1` is required to list running containers. `EVENTS` is also needed but it is enabled by default.
Running Dozzle without any certs should work. Here is an example:
```sh
docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir20/dozzle --remote-host tcp://123.1.1.1:2375
```
::: warning
Exposing `docker.sock` publicly is not safe. Only use a proxy for an internal network where all clients are trusted.
:::
+21
View File
@@ -0,0 +1,21 @@
---
title: Environment variables and configuration
---
# Environment variables and configuration
Configurations can be done with flags or environment variables. The table below outlines all supported options and their respective env vars.
| Flag | Env Variable | Default |
| ---------------- | ---------------------- | ------- |
| `--addr` | `DOZZLE_ADDR` | `:8080` |
| `--base` | `DOZZLE_BASE` | `/` |
| `--hostname` | `DOZZLE_HOSTNAME` | `""` |
| `--level` | `DOZZLE_LEVEL` | `info` |
| `--filter` | `DOZZLE_FILTER` | `""` |
| `--username` | `DOZZLE_USERNAME` | `""` |
| `--password` | `DOZZLE_PASSWORD` | `""` |
| `--usernamefile` | `DOZZLE_USERNAME_FILE` | `""` |
| `--passwordfile` | `DOZZLE_PASSWORD_FILE` | `""` |
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
| `--remote-host` | `DOZZLE_REMOTE_HOST` | |
+11
View File
@@ -0,0 +1,11 @@
---
title: What is Dozzle?
---
# What is Dozzle?
Dozzle is open-sourced project sponsored by Docker OSS. It is a log viewer designed to simplify the process of monitoring and debugging containers. It is a lightweight, web-based application that provides real-time log streaming, filtering, and searching capabilities through an intuitive user interface.
Users can quickly and easily access logs generated by their Docker containers, making it an essential tool for debugging and troubleshooting applications running in a Docker environment. Out of the box, Dozzle supports JSON logs with intelligent color coding.
Dozzle is easy to install and can be configured with minimal effort, making it an ideal solution for developers and system administrators looking for an efficient and user-friendly log viewer for their Docker environment. The tool is available under the MIT license and is actively maintained by its developer, Amir Raminfar.
+22
View File
@@ -0,0 +1,22 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
title: Home
hero:
name: "Dozzle"
tagline: A lightweight, web-based Docker log viewer that provides real-time monitoring and easy troubleshooting.
actions:
- theme: brand
text: Get Started
link: /guide/getting-started
features:
- title: Real-time logging
details: Allows you to view logs of other Docker containers in real-time. As new log entries are generated, they are streamed to the web interface without needing to refresh the page.
- title: Lightweight
details: An application written in Go consuming very little memory and CPU. It can be run alongside other containers without causing performance issues.
- title: Multi-host Support
details: Dozzle UI support connecting to multiple remote hosts with a simple drop down to choose between different hosts.
---
+12
View File
@@ -0,0 +1,12 @@
{
"private": true,
"type": "module",
"devDependencies": {
"@unocss/preset-typography": "^0.50.6",
"@unocss/reset": "^0.50.6",
"@unocss/transformer-directives": "^0.50.6",
"dozzle": "workspace:*",
"sitemap": "^7.1.1",
"unocss": "^0.50.6"
}
}
+819
View File
@@ -0,0 +1,819 @@
lockfileVersion: 5.4
specifiers:
'@unocss/preset-typography': ^0.50.6
'@unocss/reset': ^0.50.6
'@unocss/transformer-directives': ^0.50.6
dozzle: workspace:*
sitemap: ^7.1.1
unocss: ^0.50.6
devDependencies:
'@unocss/preset-typography': 0.50.6
'@unocss/reset': 0.50.6
'@unocss/transformer-directives': 0.50.6
dozzle: link:..
sitemap: 7.1.1
unocss: 0.50.6
packages:
/@ampproject/remapping/2.2.0:
resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==}
engines: {node: '>=6.0.0'}
dependencies:
'@jridgewell/gen-mapping': 0.1.1
'@jridgewell/trace-mapping': 0.3.17
dev: true
/@antfu/install-pkg/0.1.1:
resolution: {integrity: sha512-LyB/8+bSfa0DFGC06zpCEfs89/XoWZwws5ygEa5D+Xsm3OfI+aXQ86VgVG7Acyef+rSZ5HE7J8rrxzrQeM3PjQ==}
dependencies:
execa: 5.1.1
find-up: 5.0.0
dev: true
/@antfu/utils/0.5.2:
resolution: {integrity: sha512-CQkeV+oJxUazwjlHD0/3ZD08QWKuGQkhnrKo3e6ly5pd48VUpXbb77q0xMU4+vc2CkJnDS02Eq/M9ugyX20XZA==}
dev: true
/@antfu/utils/0.7.2:
resolution: {integrity: sha512-vy9fM3pIxZmX07dL+VX1aZe7ynZ+YyB0jY+jE6r3hOK6GNY2t6W8rzpFC4tgpbXUYABkFQwgJq2XYXlxbXAI0g==}
dev: true
/@iconify/types/2.0.0:
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
dev: true
/@iconify/utils/2.1.5:
resolution: {integrity: sha512-6MvDI+I6QMvXn5rK9KQGdpEE4mmLTcuQdLZEiX5N+uZB+vc4Yw9K1OtnOgkl8mp4d9X0UrILREyZgF1NUwUt+Q==}
dependencies:
'@antfu/install-pkg': 0.1.1
'@antfu/utils': 0.7.2
'@iconify/types': 2.0.0
debug: 4.3.4
kolorist: 1.7.0
local-pkg: 0.4.3
transitivePeerDependencies:
- supports-color
dev: true
/@jridgewell/gen-mapping/0.1.1:
resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==}
engines: {node: '>=6.0.0'}
dependencies:
'@jridgewell/set-array': 1.1.2
'@jridgewell/sourcemap-codec': 1.4.14
dev: true
/@jridgewell/resolve-uri/3.1.0:
resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==}
engines: {node: '>=6.0.0'}
dev: true
/@jridgewell/set-array/1.1.2:
resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
engines: {node: '>=6.0.0'}
dev: true
/@jridgewell/sourcemap-codec/1.4.14:
resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==}
dev: true
/@jridgewell/trace-mapping/0.3.17:
resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==}
dependencies:
'@jridgewell/resolve-uri': 3.1.0
'@jridgewell/sourcemap-codec': 1.4.14
dev: true
/@nodelib/fs.scandir/2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
dependencies:
'@nodelib/fs.stat': 2.0.5
run-parallel: 1.2.0
dev: true
/@nodelib/fs.stat/2.0.5:
resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
engines: {node: '>= 8'}
dev: true
/@nodelib/fs.walk/1.2.8:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
dependencies:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.15.0
dev: true
/@polka/url/1.0.0-next.21:
resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==}
dev: true
/@rollup/pluginutils/5.0.2:
resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0
peerDependenciesMeta:
rollup:
optional: true
dependencies:
'@types/estree': 1.0.0
estree-walker: 2.0.2
picomatch: 2.3.1
dev: true
/@types/estree/1.0.0:
resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==}
dev: true
/@types/node/17.0.45:
resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==}
dev: true
/@types/node/18.14.2:
resolution: {integrity: sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==}
dev: true
/@types/sax/1.2.4:
resolution: {integrity: sha512-pSAff4IAxJjfAXUG6tFkO7dsSbTmf8CtUpfhhZ5VhkRpC4628tJhh3+V6H1E+/Gs9piSzYKT5yzHO5M4GG9jkw==}
dependencies:
'@types/node': 18.14.2
dev: true
/@unocss/astro/0.50.6:
resolution: {integrity: sha512-gSGQIh+hBCor7KbAylu4wBQaMZp3AkT8dW9E6jrecpluVxzGGdar93a79Wqs76OlWiu7hr8zOyRbSDgfkwDung==}
dependencies:
'@unocss/core': 0.50.6
'@unocss/reset': 0.50.6
'@unocss/vite': 0.50.6
transitivePeerDependencies:
- rollup
- vite
dev: true
/@unocss/cli/0.50.6:
resolution: {integrity: sha512-La/KeZCpI7WxuqiUj37K7k/mh08oIGm15u8pkHUs2z+XtFWLemjWPeu84NK3cLgyUGlO2nwpDm2Awye4G1GgCg==}
engines: {node: '>=14'}
hasBin: true
dependencies:
'@ampproject/remapping': 2.2.0
'@rollup/pluginutils': 5.0.2
'@unocss/config': 0.50.6
'@unocss/core': 0.50.6
'@unocss/preset-uno': 0.50.6
cac: 6.7.14
chokidar: 3.5.3
colorette: 2.0.19
consola: 2.15.3
fast-glob: 3.2.12
magic-string: 0.30.0
pathe: 1.1.0
perfect-debounce: 0.1.3
transitivePeerDependencies:
- rollup
dev: true
/@unocss/config/0.50.6:
resolution: {integrity: sha512-/IdnXyU4NOQCXBryZsEv9GYAnTvCZ/wmm5mv5ZIPXrS1ZClVbCbnwUxIW08t4EHIX/E9gSFClzXJ52pLBFkZ7g==}
engines: {node: '>=14'}
dependencies:
'@unocss/core': 0.50.6
unconfig: 0.3.7
dev: true
/@unocss/core/0.50.6:
resolution: {integrity: sha512-WMIp8xr7YSlID2whqfRGLwagp59e6u4ckPACEpoDOW8sTeSPRZm54hxPhuWXD1SQuqcwHPMtM9nzGD8UOnqQxA==}
dev: true
/@unocss/inspector/0.50.6:
resolution: {integrity: sha512-6nX1YtaL67ohn/PfSSBv3npJ8qZcdc7S9X2zE6PUD/xhwtz7Bohx9I/KtmFdjJz5WeeGR7di0uYC6xsAcFLndQ==}
dependencies:
gzip-size: 6.0.0
sirv: 2.0.2
dev: true
/@unocss/postcss/0.50.6:
resolution: {integrity: sha512-pRPBVPmwjsVu3v1T0hQuqq3L4K74Wobo6pGDypvK/MuzWdWDhHiktWwmXGNxlYSWK7mGJBIa+vI10pp4e15OUw==}
engines: {node: '>=14'}
dependencies:
'@unocss/config': 0.50.6
'@unocss/core': 0.50.6
css-tree: 2.3.1
fast-glob: 3.2.12
magic-string: 0.30.0
postcss: 8.4.21
dev: true
/@unocss/preset-attributify/0.50.6:
resolution: {integrity: sha512-hLxBoOnwMLILhJ5fM2AtnjSWej2GffIjTdrAHIBctEy8sLssou5lIbSukDptuGRTSwkishuvQhH020dptB3YJw==}
dependencies:
'@unocss/core': 0.50.6
dev: true
/@unocss/preset-icons/0.50.6:
resolution: {integrity: sha512-ADnvX8JCThEr2DBiDprTh2q4bfvULbCg9hL5DZIuP9/31FvzNqpAI8xo9KpsCPBoQMksg+GVoXhj1CN2IxLhtg==}
dependencies:
'@iconify/utils': 2.1.5
'@unocss/core': 0.50.6
ofetch: 1.0.1
transitivePeerDependencies:
- supports-color
dev: true
/@unocss/preset-mini/0.50.6:
resolution: {integrity: sha512-Ejgib688uvzCVgT/DHAOyXxKcM8vX55mxh8m3GAEx1H1pxg0IBfJO4QCKa3uAnasxj27XescBbvqv04dWi+jEQ==}
dependencies:
'@unocss/core': 0.50.6
dev: true
/@unocss/preset-tagify/0.50.6:
resolution: {integrity: sha512-ZyG/SJMobn4GZMbgrZOxT59ARp22LwgJGArCwJVosh3rraRVlb+B4x6ctMl6JOiLG5B1lHT9vZ92//u51Y0WTw==}
dependencies:
'@unocss/core': 0.50.6
dev: true
/@unocss/preset-typography/0.50.6:
resolution: {integrity: sha512-5WBsvHHVBBNJVoh38GwkJpcfS+JTdi0KuQuTsz6SvhQg+xuth3a6ebhAlrEpdS2G9BL5lpMwBd9SbgvpkE75Og==}
dependencies:
'@unocss/core': 0.50.6
'@unocss/preset-mini': 0.50.6
dev: true
/@unocss/preset-uno/0.50.6:
resolution: {integrity: sha512-MTuTd49mU8CfKGP/SVMqxNBDEXJmJPes+K6uwZnOsoEwySh1ZrGdApwmIeJurlDEsBiQFQfaSHKHQ25JWVPg3A==}
dependencies:
'@unocss/core': 0.50.6
'@unocss/preset-mini': 0.50.6
'@unocss/preset-wind': 0.50.6
dev: true
/@unocss/preset-web-fonts/0.50.6:
resolution: {integrity: sha512-81meQMAq2lOy7k5qHQZ2EGWN5iJQUJOLl8dc9dxIo1eZPgiZQruxTVr4AkNVH5LRFcjHs/1sDb2CYxAiakwTVg==}
dependencies:
'@unocss/core': 0.50.6
ofetch: 1.0.1
dev: true
/@unocss/preset-wind/0.50.6:
resolution: {integrity: sha512-LU5fAad+okonKfP1eHY5Q63uhoiSstQf4lU7CZ97eZdduqN1ODLzeBegjMGHHi1K2112AvzN/Au0WEPjQFgf7Q==}
dependencies:
'@unocss/core': 0.50.6
'@unocss/preset-mini': 0.50.6
dev: true
/@unocss/reset/0.50.6:
resolution: {integrity: sha512-e1fuSEgp1p7FgpsIZKNejOKgq4gyZcDGDvi+6544x458hInM6MfiMQNP95UBJEG4JZXq6qCZ8t7tRVWS2m5IXg==}
dev: true
/@unocss/scope/0.50.6:
resolution: {integrity: sha512-ep1RRJzA3xbVmEcsBYvY8i/LQIfoCFaw1bx9hvGLhdCIdboy58sL4R4GigOukBJiufykKkrLNzFmlIsi3EfJWg==}
dev: true
/@unocss/transformer-attributify-jsx/0.50.6:
resolution: {integrity: sha512-nXt9Kj9L72ehiDwkzjzxXV4QcV7VS60kP4PXuf+Gp70wE3/19iZl3K3Um/lGo3WHgAYvr91hxBkcyxYBOLUIFA==}
dependencies:
'@unocss/core': 0.50.6
dev: true
/@unocss/transformer-compile-class/0.50.6:
resolution: {integrity: sha512-MWpxO94bq28+HWxniED4BP26aIs5hGhTnaTBkTrisiF8T9xyNPwXaSUobuYkjP1s3ojrXHha8ic0uIXy5OC5nw==}
dependencies:
'@unocss/core': 0.50.6
dev: true
/@unocss/transformer-directives/0.50.6:
resolution: {integrity: sha512-deUJMZDDYQ/kGRFdGe3JrmnbiScs9Smc5Vj35Pr/MNMAM9pItbMwCgZLc+EcHgYNvO5tp0qfA9prPAIs4YpRbg==}
dependencies:
'@unocss/core': 0.50.6
css-tree: 2.3.1
dev: true
/@unocss/transformer-variant-group/0.50.6:
resolution: {integrity: sha512-eAbJ2Ho1lYaFvpKexqUqixh2TvzcE/DNSIHncr/9Ezo9s06kkHxw4KeIxw//TF26zYP7IJx8l5F58HoSjdKwbQ==}
dependencies:
'@unocss/core': 0.50.6
dev: true
/@unocss/vite/0.50.6:
resolution: {integrity: sha512-BBfNHWRTD69ToNX4NlYdORFG6uH51HCjX+vZ8HAVgYHpSeVWziG3srnGYOk5IS0pKPzQGoLBlz8rstMsGhrAjA==}
peerDependencies:
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0
dependencies:
'@ampproject/remapping': 2.2.0
'@rollup/pluginutils': 5.0.2
'@unocss/config': 0.50.6
'@unocss/core': 0.50.6
'@unocss/inspector': 0.50.6
'@unocss/scope': 0.50.6
'@unocss/transformer-directives': 0.50.6
chokidar: 3.5.3
fast-glob: 3.2.12
magic-string: 0.30.0
transitivePeerDependencies:
- rollup
dev: true
/anymatch/3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
dev: true
/arg/5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
dev: true
/binary-extensions/2.2.0:
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
engines: {node: '>=8'}
dev: true
/braces/3.0.2:
resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
engines: {node: '>=8'}
dependencies:
fill-range: 7.0.1
dev: true
/cac/6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
dev: true
/chokidar/3.5.3:
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
engines: {node: '>= 8.10.0'}
dependencies:
anymatch: 3.1.3
braces: 3.0.2
glob-parent: 5.1.2
is-binary-path: 2.1.0
is-glob: 4.0.3
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
fsevents: 2.3.2
dev: true
/colorette/2.0.19:
resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==}
dev: true
/consola/2.15.3:
resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==}
dev: true
/cross-spawn/7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
dev: true
/css-tree/2.3.1:
resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
dependencies:
mdn-data: 2.0.30
source-map-js: 1.0.2
dev: true
/debug/4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.1.2
dev: true
/defu/6.1.2:
resolution: {integrity: sha512-+uO4+qr7msjNNWKYPHqN/3+Dx3NFkmIzayk2L1MyZQlvgZb/J1A0fo410dpKrN2SnqFjt8n4JL8fDJE0wIgjFQ==}
dev: true
/destr/1.2.2:
resolution: {integrity: sha512-lrbCJwD9saUQrqUfXvl6qoM+QN3W7tLV5pAOs+OqOmopCCz/JkE05MHedJR1xfk4IAnZuJXPVuN5+7jNA2ZCiA==}
dev: true
/duplexer/0.1.2:
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
dev: true
/estree-walker/2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
dev: true
/execa/5.1.1:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
dependencies:
cross-spawn: 7.0.3
get-stream: 6.0.1
human-signals: 2.1.0
is-stream: 2.0.1
merge-stream: 2.0.0
npm-run-path: 4.0.1
onetime: 5.1.2
signal-exit: 3.0.7
strip-final-newline: 2.0.0
dev: true
/fast-glob/3.2.12:
resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==}
engines: {node: '>=8.6.0'}
dependencies:
'@nodelib/fs.stat': 2.0.5
'@nodelib/fs.walk': 1.2.8
glob-parent: 5.1.2
merge2: 1.4.1
micromatch: 4.0.5
dev: true
/fastq/1.15.0:
resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==}
dependencies:
reusify: 1.0.4
dev: true
/fill-range/7.0.1:
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
engines: {node: '>=8'}
dependencies:
to-regex-range: 5.0.1
dev: true
/find-up/5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
dependencies:
locate-path: 6.0.0
path-exists: 4.0.0
dev: true
/fsevents/2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
requiresBuild: true
dev: true
optional: true
/get-stream/6.0.1:
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
engines: {node: '>=10'}
dev: true
/glob-parent/5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
dependencies:
is-glob: 4.0.3
dev: true
/gzip-size/6.0.0:
resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==}
engines: {node: '>=10'}
dependencies:
duplexer: 0.1.2
dev: true
/human-signals/2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
dev: true
/is-binary-path/2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
dependencies:
binary-extensions: 2.2.0
dev: true
/is-extglob/2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
dev: true
/is-glob/4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
dependencies:
is-extglob: 2.1.1
dev: true
/is-number/7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
dev: true
/is-stream/2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'}
dev: true
/isexe/2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
dev: true
/jiti/1.18.2:
resolution: {integrity: sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==}
hasBin: true
dev: true
/kolorist/1.7.0:
resolution: {integrity: sha512-ymToLHqL02udwVdbkowNpzjFd6UzozMtshPQKVi5k1EjKRqKqBrOnE9QbLEb0/pV76SAiIT13hdL8R6suc+f3g==}
dev: true
/local-pkg/0.4.3:
resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==}
engines: {node: '>=14'}
dev: true
/locate-path/6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
dependencies:
p-locate: 5.0.0
dev: true
/magic-string/0.30.0:
resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==}
engines: {node: '>=12'}
dependencies:
'@jridgewell/sourcemap-codec': 1.4.14
dev: true
/mdn-data/2.0.30:
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
dev: true
/merge-stream/2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
dev: true
/merge2/1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
dev: true
/micromatch/4.0.5:
resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
engines: {node: '>=8.6'}
dependencies:
braces: 3.0.2
picomatch: 2.3.1
dev: true
/mimic-fn/2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
dev: true
/mrmime/1.0.1:
resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==}
engines: {node: '>=10'}
dev: true
/ms/2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: true
/nanoid/3.3.4:
resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
dev: true
/node-fetch-native/1.0.2:
resolution: {integrity: sha512-KIkvH1jl6b3O7es/0ShyCgWLcfXxlBrLBbP3rOr23WArC66IMcU4DeZEeYEOwnopYhawLTn7/y+YtmASe8DFVQ==}
dev: true
/normalize-path/3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
dev: true
/npm-run-path/4.0.1:
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
engines: {node: '>=8'}
dependencies:
path-key: 3.1.1
dev: true
/ofetch/1.0.1:
resolution: {integrity: sha512-icBz2JYfEpt+wZz1FRoGcrMigjNKjzvufE26m9+yUiacRQRHwnNlGRPiDnW4op7WX/MR6aniwS8xw8jyVelF2g==}
dependencies:
destr: 1.2.2
node-fetch-native: 1.0.2
ufo: 1.1.1
dev: true
/onetime/5.1.2:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'}
dependencies:
mimic-fn: 2.1.0
dev: true
/p-limit/3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
dependencies:
yocto-queue: 0.1.0
dev: true
/p-locate/5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
dependencies:
p-limit: 3.1.0
dev: true
/path-exists/4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
dev: true
/path-key/3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
dev: true
/pathe/1.1.0:
resolution: {integrity: sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==}
dev: true
/perfect-debounce/0.1.3:
resolution: {integrity: sha512-NOT9AcKiDGpnV/HBhI22Str++XWcErO/bALvHCuhv33owZW/CjH8KAFLZDCmu3727sihe0wTxpDhyGc6M8qacQ==}
dev: true
/picocolors/1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
dev: true
/picomatch/2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
dev: true
/postcss/8.4.21:
resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==}
engines: {node: ^10 || ^12 || >=14}
dependencies:
nanoid: 3.3.4
picocolors: 1.0.0
source-map-js: 1.0.2
dev: true
/queue-microtask/1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
dev: true
/readdirp/3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
dependencies:
picomatch: 2.3.1
dev: true
/reusify/1.0.4:
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
dev: true
/run-parallel/1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
dependencies:
queue-microtask: 1.2.3
dev: true
/sax/1.2.4:
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
dev: true
/shebang-command/2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
dependencies:
shebang-regex: 3.0.0
dev: true
/shebang-regex/3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
dev: true
/signal-exit/3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
dev: true
/sirv/2.0.2:
resolution: {integrity: sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==}
engines: {node: '>= 10'}
dependencies:
'@polka/url': 1.0.0-next.21
mrmime: 1.0.1
totalist: 3.0.0
dev: true
/sitemap/7.1.1:
resolution: {integrity: sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg==}
engines: {node: '>=12.0.0', npm: '>=5.6.0'}
hasBin: true
dependencies:
'@types/node': 17.0.45
'@types/sax': 1.2.4
arg: 5.0.2
sax: 1.2.4
dev: true
/source-map-js/1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'}
dev: true
/strip-final-newline/2.0.0:
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
engines: {node: '>=6'}
dev: true
/to-regex-range/5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
dependencies:
is-number: 7.0.0
dev: true
/totalist/3.0.0:
resolution: {integrity: sha512-eM+pCBxXO/njtF7vdFsHuqb+ElbxqtI4r5EAvk6grfAFyJ6IvWlSkfZ5T9ozC6xWw3Fj1fGoSmrl0gUs46JVIw==}
engines: {node: '>=6'}
dev: true
/ufo/1.1.1:
resolution: {integrity: sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==}
dev: true
/unconfig/0.3.7:
resolution: {integrity: sha512-1589b7oGa8ILBYpta7TndM5mLHLzHUqBfhszeZxuUBrjO/RoQ52VGVWsS3w0C0GLNxO9RPmqkf6BmIvBApaRdA==}
dependencies:
'@antfu/utils': 0.5.2
defu: 6.1.2
jiti: 1.18.2
dev: true
/unocss/0.50.6:
resolution: {integrity: sha512-7cKiIB/ssAPvCDUcFMs0jm0FzIyQKfgIjUzBYZ5dVFthOvN5dcFh7bCZE9dIM862n7oW8FjbkTxwdTbRqqJQVQ==}
engines: {node: '>=14'}
peerDependencies:
'@unocss/webpack': 0.50.6
peerDependenciesMeta:
'@unocss/webpack':
optional: true
dependencies:
'@unocss/astro': 0.50.6
'@unocss/cli': 0.50.6
'@unocss/core': 0.50.6
'@unocss/postcss': 0.50.6
'@unocss/preset-attributify': 0.50.6
'@unocss/preset-icons': 0.50.6
'@unocss/preset-mini': 0.50.6
'@unocss/preset-tagify': 0.50.6
'@unocss/preset-typography': 0.50.6
'@unocss/preset-uno': 0.50.6
'@unocss/preset-web-fonts': 0.50.6
'@unocss/preset-wind': 0.50.6
'@unocss/reset': 0.50.6
'@unocss/transformer-attributify-jsx': 0.50.6
'@unocss/transformer-compile-class': 0.50.6
'@unocss/transformer-directives': 0.50.6
'@unocss/transformer-variant-group': 0.50.6
'@unocss/vite': 0.50.6
transitivePeerDependencies:
- rollup
- supports-color
- vite
dev: true
/which/2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
hasBin: true
dependencies:
isexe: 2.0.0
dev: true
/yocto-queue/0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
dev: true
+3
View File
@@ -0,0 +1,3 @@
/assets/*
cache-control: max-age=31536000
cache-control: immutable
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

+32
View File
@@ -0,0 +1,32 @@
import { defineConfig, presetAttributify, presetIcons, presetUno, presetWebFonts } from "unocss";
import { presetTypography } from "@unocss/preset-typography";
import transformerDirectives from "@unocss/transformer-directives";
export default defineConfig({
shortcuts: [[/^circle-(\w+)$/, ([, c]) => `rounded-full bg-${c}500 w-3 h-3`]],
transformers: [transformerDirectives()],
presets: [
presetUno(),
presetAttributify(),
presetTypography(),
presetIcons({
scale: 1.2,
warn: true,
}),
presetWebFonts({
fonts: {
sans: "Roboto:200",
playfair: [
{
name: "Playfair Display",
weights: [100, 200, 400, 700],
},
],
},
}),
],
theme: {
colors: {},
},
});
+18
View File
@@ -0,0 +1,18 @@
import path from "path";
import { defineConfig } from "vite";
import Components from "unplugin-vue-components/vite";
import Unocss from "unocss/vite";
export default defineConfig({
plugins: [
Components({
dirs: [path.resolve(__dirname, ".vitepress/theme/components")],
extensions: ["vue", "md"],
include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
dts: true,
}),
Unocss(),
],
});
+3 -3
View File
@@ -1,6 +1,6 @@
FROM cypress/included:12.0.2
FROM cypress/included:12.8.1
RUN apt install curl && curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
RUN npm i -g pnpm
WORKDIR /e2e
@@ -8,4 +8,4 @@ COPY pnpm-lock.yaml ./
RUN pnpm fetch
COPY package.json tsconfig.json ./
RUN pnpm install -r --offline
RUN pnpm install --offline
+1 -1
View File
@@ -1,5 +1,5 @@
import { defineConfig } from "cypress";
import { initPlugin } from '@frsource/cypress-plugin-visual-regression-diff/dist/plugins';
import { initPlugin } from "@frsource/cypress-plugin-visual-regression-diff/dist/plugins";
export default defineConfig({
fixturesFolder: false,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

+2 -2
View File
@@ -14,8 +14,8 @@
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
import '@frsource/cypress-plugin-visual-regression-diff/dist/support';
import "./commands";
import "@frsource/cypress-plugin-visual-regression-diff/dist/support";
// Alternatively you can use CommonJS syntax:
// require('./commands')
+3 -3
View File
@@ -6,8 +6,8 @@
},
"license": "ISC",
"dependencies": {
"@frsource/cypress-plugin-visual-regression-diff": "^3.2.5",
"cypress": "^12.0.2",
"typescript": "^4.9.4"
"@frsource/cypress-plugin-visual-regression-diff": "^3.2.8",
"cypress": "^12.8.1",
"typescript": "^5.0.2"
}
}
+106 -61
View File
@@ -1,14 +1,14 @@
lockfileVersion: 5.4
specifiers:
'@frsource/cypress-plugin-visual-regression-diff': ^3.2.5
cypress: ^12.0.2
typescript: ^4.9.4
'@frsource/cypress-plugin-visual-regression-diff': ^3.2.8
cypress: ^12.8.1
typescript: ^5.0.2
dependencies:
'@frsource/cypress-plugin-visual-regression-diff': 3.2.5_cypress@12.0.2
cypress: 12.0.2
typescript: 4.9.4
'@frsource/cypress-plugin-visual-regression-diff': 3.2.8_cypress@12.8.1
cypress: 12.8.1
typescript: 5.0.2
packages:
@@ -19,12 +19,12 @@ packages:
dev: false
optional: true
/@cypress/request/2.88.10:
resolution: {integrity: sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg==}
/@cypress/request/2.88.11:
resolution: {integrity: sha512-M83/wfQ1EkspjkE2lNWNV5ui2Cv7UCv1swW1DqljahbzLVWltcsexQh8jYtuS/vzFXP+HySntGM83ZXA9fn17w==}
engines: {node: '>= 6'}
dependencies:
aws-sign2: 0.7.0
aws4: 1.11.0
aws4: 1.12.0
caseless: 0.12.0
combined-stream: 1.0.8
extend: 3.0.2
@@ -36,7 +36,7 @@ packages:
json-stringify-safe: 5.0.1
mime-types: 2.1.35
performance-now: 2.1.0
qs: 6.5.3
qs: 6.10.4
safe-buffer: 5.2.1
tough-cookie: 2.5.0
tunnel-agent: 0.6.0
@@ -56,14 +56,14 @@ packages:
resolution: {integrity: sha512-RIlQtKezeRhNuxIpIc2BuM4kNqAakIarKV/TCgx2EC+zgj1NxX3+Mc80AkJCCMO8dZUuGzbULG7295YeBQZlSA==}
dev: false
/@frsource/cypress-plugin-visual-regression-diff/3.2.5_cypress@12.0.2:
resolution: {integrity: sha512-mN0IpyqRZoj2xm6S63iRHz4WilDgR7vZcCBZpYEp4IjjLb33VKjtbmaErKpy501t78tqvlBIA/93hSMs3sZPLQ==}
/@frsource/cypress-plugin-visual-regression-diff/3.2.8_cypress@12.8.1:
resolution: {integrity: sha512-B7TbwdAiql73FxRr2tiFmx8PvpK4a2oCBygPzT1v9IvCxh4XwoXwY47/klKtg2402PrhdXwcqI/NBSVqb4CJnQ==}
engines: {node: '>=10'}
peerDependencies:
cypress: '>=4.5.0'
dependencies:
'@frsource/base64': 1.0.5
cypress: 12.0.2
cypress: 12.8.1
glob: 8.0.3
meta-png: 1.0.5
move-file: 2.1.0
@@ -72,8 +72,8 @@ packages:
sharp: 0.31.2
dev: false
/@types/node/14.18.34:
resolution: {integrity: sha512-hcU9AIQVHmPnmjRK+XUUYlILlr9pQrsqSrwov/JK1pnf3GTQowVBhx54FbvM0AU/VXGH4i3+vgXS5EguR7fysA==}
/@types/node/14.18.38:
resolution: {integrity: sha512-zMRIidN2Huikv/+/U7gRPFYsXDR/7IGqFZzTLnCEj5+gkrQjsowfamaxEnyvArct5hxGA3bTxMXlYhH78V6Cew==}
dev: false
/@types/sinonjs__fake-timers/8.1.1:
@@ -88,7 +88,7 @@ packages:
resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==}
requiresBuild: true
dependencies:
'@types/node': 14.18.34
'@types/node': 14.18.38
dev: false
optional: true
@@ -161,8 +161,8 @@ packages:
resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==}
dev: false
/aws4/1.11.0:
resolution: {integrity: sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==}
/aws4/1.12.0:
resolution: {integrity: sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==}
dev: false
/balanced-match/1.0.2:
@@ -184,7 +184,7 @@ packages:
dependencies:
buffer: 5.7.1
inherits: 2.0.4
readable-stream: 3.6.0
readable-stream: 3.6.2
dev: false
/blob-util/2.0.2:
@@ -224,6 +224,13 @@ packages:
engines: {node: '>=6'}
dev: false
/call-bind/1.0.2:
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
dependencies:
function-bind: 1.1.1
get-intrinsic: 1.2.0
dev: false
/caseless/0.12.0:
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
dev: false
@@ -245,8 +252,8 @@ packages:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
dev: false
/ci-info/3.7.0:
resolution: {integrity: sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog==}
/ci-info/3.8.0:
resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==}
engines: {node: '>=8'}
dev: false
@@ -343,15 +350,15 @@ packages:
which: 2.0.2
dev: false
/cypress/12.0.2:
resolution: {integrity: sha512-WnLx1DpnbF1vbpDBkgP14rK5yS3U+Gvxrv2fsB4Owma26oIyENj7DDRnsJbSZuTfG4mcuUJxAkRHJR2wBqBfMA==}
/cypress/12.8.1:
resolution: {integrity: sha512-lIFbKdaSYAOarNLHNFa2aPZu6YSF+8UY4VRXMxJrFUnk6RvfG0AWsZ7/qle/aIz30TNUD4aOihz2ZgS4vuQVSA==}
engines: {node: ^14.0.0 || ^16.0.0 || >=18.0.0}
hasBin: true
requiresBuild: true
dependencies:
'@cypress/request': 2.88.10
'@cypress/request': 2.88.11
'@cypress/xvfb': 1.2.4_supports-color@8.1.1
'@types/node': 14.18.34
'@types/node': 14.18.38
'@types/sinonjs__fake-timers': 8.1.1
'@types/sizzle': 2.3.3
arch: 2.2.0
@@ -381,7 +388,7 @@ packages:
listr2: 3.14.0_enquirer@2.3.6
lodash: 4.17.21
log-symbols: 4.1.0
minimist: 1.2.7
minimist: 1.2.8
ospath: 1.2.2
pretty-bytes: 5.6.0
proxy-from-env: 1.0.0
@@ -569,7 +576,7 @@ packages:
engines: {node: '>=10'}
dependencies:
at-least-node: 1.0.0
graceful-fs: 4.2.10
graceful-fs: 4.2.11
jsonfile: 6.1.0
universalify: 2.0.0
dev: false
@@ -578,6 +585,18 @@ packages:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
dev: false
/function-bind/1.1.1:
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
dev: false
/get-intrinsic/1.2.0:
resolution: {integrity: sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==}
dependencies:
function-bind: 1.1.1
has: 1.0.3
has-symbols: 1.0.3
dev: false
/get-stream/5.2.0:
resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==}
engines: {node: '>=8'}
@@ -619,7 +638,7 @@ packages:
fs.realpath: 1.0.0
inflight: 1.0.6
inherits: 2.0.4
minimatch: 5.1.1
minimatch: 5.1.6
once: 1.4.0
dev: false
@@ -630,8 +649,8 @@ packages:
ini: 2.0.0
dev: false
/graceful-fs/4.2.10:
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
/graceful-fs/4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
dev: false
/has-flag/4.0.0:
@@ -639,6 +658,18 @@ packages:
engines: {node: '>=8'}
dev: false
/has-symbols/1.0.3:
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
engines: {node: '>= 0.4'}
dev: false
/has/1.0.3:
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
engines: {node: '>= 0.4.0'}
dependencies:
function-bind: 1.1.1
dev: false
/http-signature/1.3.6:
resolution: {integrity: sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==}
engines: {node: '>=0.10'}
@@ -690,7 +721,7 @@ packages:
resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==}
hasBin: true
dependencies:
ci-info: 3.7.0
ci-info: 3.8.0
dev: false
/is-fullwidth-code-point/3.0.0:
@@ -750,7 +781,7 @@ packages:
dependencies:
universalify: 2.0.0
optionalDependencies:
graceful-fs: 4.2.10
graceful-fs: 4.2.11
dev: false
/jsprim/2.0.2:
@@ -783,7 +814,7 @@ packages:
log-update: 4.0.0
p-map: 4.0.0
rfdc: 1.3.0
rxjs: 7.6.0
rxjs: 7.8.0
through: 2.3.8
wrap-ansi: 7.0.0
dev: false
@@ -857,15 +888,15 @@ packages:
brace-expansion: 1.1.11
dev: false
/minimatch/5.1.1:
resolution: {integrity: sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g==}
/minimatch/5.1.6:
resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
engines: {node: '>=10'}
dependencies:
brace-expansion: 2.0.1
dev: false
/minimist/1.2.7:
resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==}
/minimist/1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
dev: false
/mkdirp-classic/0.5.3:
@@ -891,15 +922,15 @@ packages:
resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==}
dev: false
/node-abi/3.30.0:
resolution: {integrity: sha512-qWO5l3SCqbwQavymOmtTVuCWZE23++S+rxyoHjXqUmPyzRcaoI4lA2gO55/drddGnedAyjA7sk76SfQ5lfUMnw==}
/node-abi/3.33.0:
resolution: {integrity: sha512-7GGVawqyHF4pfd0YFybhv/eM9JwTtPqx0mAanQ146O3FlSh3pA24zf9IRQTOsfTSqXTNzPSP5iagAJ94jjuVog==}
engines: {node: '>=10'}
dependencies:
semver: 7.3.8
dev: false
/node-addon-api/5.0.0:
resolution: {integrity: sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==}
/node-addon-api/5.1.0:
resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==}
dev: false
/npm-run-path/4.0.1:
@@ -909,6 +940,10 @@ packages:
path-key: 3.1.1
dev: false
/object-inspect/1.12.3:
resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==}
dev: false
/once/1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
dependencies:
@@ -981,10 +1016,10 @@ packages:
detect-libc: 2.0.1
expand-template: 2.0.3
github-from-package: 0.0.0
minimist: 1.2.7
minimist: 1.2.8
mkdirp-classic: 0.5.3
napi-build-utils: 1.0.2
node-abi: 3.30.0
node-abi: 3.33.0
pump: 3.0.0
rc: 1.2.8
simple-get: 4.0.1
@@ -1012,14 +1047,16 @@ packages:
once: 1.4.0
dev: false
/punycode/2.1.1:
resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==}
/punycode/2.3.0:
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
engines: {node: '>=6'}
dev: false
/qs/6.5.3:
resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==}
/qs/6.10.4:
resolution: {integrity: sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==}
engines: {node: '>=0.6'}
dependencies:
side-channel: 1.0.4
dev: false
/rc/1.2.8:
@@ -1028,12 +1065,12 @@ packages:
dependencies:
deep-extend: 0.6.0
ini: 1.3.8
minimist: 1.2.7
minimist: 1.2.8
strip-json-comments: 2.0.1
dev: false
/readable-stream/3.6.0:
resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==}
/readable-stream/3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
dependencies:
inherits: 2.0.4
@@ -1066,10 +1103,10 @@ packages:
glob: 7.2.3
dev: false
/rxjs/7.6.0:
resolution: {integrity: sha512-DDa7d8TFNUalGC9VqXvQ1euWNN7sc63TrUCuM9J998+ViviahMIjKSOU7rfcgFOF+FCD71BhDRv4hrFz+ImDLQ==}
/rxjs/7.8.0:
resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==}
dependencies:
tslib: 2.4.1
tslib: 2.5.0
dev: false
/safe-buffer/5.2.1:
@@ -1095,7 +1132,7 @@ packages:
dependencies:
color: 4.2.3
detect-libc: 2.0.1
node-addon-api: 5.0.0
node-addon-api: 5.1.0
prebuild-install: 7.1.1
semver: 7.3.8
simple-get: 4.0.1
@@ -1115,6 +1152,14 @@ packages:
engines: {node: '>=8'}
dev: false
/side-channel/1.0.4:
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
dependencies:
call-bind: 1.0.2
get-intrinsic: 1.2.0
object-inspect: 1.12.3
dev: false
/signal-exit/3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
dev: false
@@ -1234,7 +1279,7 @@ packages:
end-of-stream: 1.4.4
fs-constants: 1.0.0
inherits: 2.0.4
readable-stream: 3.6.0
readable-stream: 3.6.2
dev: false
/throttleit/1.0.0:
@@ -1257,11 +1302,11 @@ packages:
engines: {node: '>=0.8'}
dependencies:
psl: 1.9.0
punycode: 2.1.1
punycode: 2.3.0
dev: false
/tslib/2.4.1:
resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==}
/tslib/2.5.0:
resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==}
dev: false
/tunnel-agent/0.6.0:
@@ -1279,9 +1324,9 @@ packages:
engines: {node: '>=10'}
dev: false
/typescript/4.9.4:
resolution: {integrity: sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==}
engines: {node: '>=4.2.0'}
/typescript/5.0.2:
resolution: {integrity: sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==}
engines: {node: '>=12.20'}
hasBin: true
dev: false
+14 -12
View File
@@ -1,14 +1,14 @@
module github.com/amir20/dozzle
require (
github.com/Microsoft/go-winio v0.5.1 // indirect
github.com/Microsoft/go-winio v0.6.0 // indirect
github.com/alexflint/go-arg v1.4.3
github.com/beme/abide v0.0.0-20190723115211-635a09831760
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v20.10.21+incompatible
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker v23.0.1+incompatible
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/dustin/go-humanize v1.0.0
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1
github.com/gogo/protobuf v1.3.2 // indirect
github.com/gorilla/mux v1.8.0
github.com/gorilla/sessions v1.2.1
@@ -16,27 +16,29 @@ require (
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/sirupsen/logrus v1.9.0
github.com/spf13/afero v1.9.3
github.com/spf13/afero v1.9.5
github.com/stretchr/objx v0.5.0 // indirect
github.com/stretchr/testify v1.8.1
golang.org/x/net v0.0.0-20211104170005-ce137452f963 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
github.com/stretchr/testify v1.8.2
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require (
github.com/alexflint/go-scalar v1.1.0 // indirect
github.com/alexflint/go-scalar v1.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/go-cmp v0.5.5 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/text v0.3.6 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
golang.org/x/tools v0.6.0 // indirect
gotest.tools/v3 v3.0.3 // indirect
)
+35 -24
View File
@@ -40,12 +40,13 @@ github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7O
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY=
github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
github.com/alexflint/go-arg v1.4.3 h1:9rwwEBpMXfKQKceuZfYcwuc/7YY7tWJbFsgG5cAU/uo=
github.com/alexflint/go-arg v1.4.3/go.mod h1:3PZ/wp/8HuqRZMUUgu7I+e1qcpUbvmS258mRXkFH4IA=
github.com/alexflint/go-scalar v1.1.0 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4PnltbhM=
github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
github.com/beme/abide v0.0.0-20190723115211-635a09831760 h1:FvTM5NSN5HYvfKpgL+8x73U5v063vHsd7AX05eV1DnM=
github.com/beme/abide v0.0.0-20190723115211-635a09831760/go.mod h1:6+8gCKsZnxzhGTmKRh4BSkLos9CbWRJNcrp55We4SqQ=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -60,16 +61,16 @@ github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v20.10.21+incompatible h1:UTLdBmHk3bEY+w8qeO5KttOhy6OmXWsl/FEet9Uswog=
github.com/docker/docker v20.10.21+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v23.0.1+incompatible h1:vjgvJZxprTTE1A37nm+CLNAdwu6xZekyoiVlUZEINcY=
github.com/docker/docker v23.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -158,8 +159,6 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk=
@@ -168,8 +167,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -180,13 +179,14 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw=
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/afero v1.9.4 h1:Sd43wM1IWz/s1aVXdOBkjJvuP8UdyqioeE4AmM0QsBs=
github.com/spf13/afero v1.9.4/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -200,6 +200,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -217,6 +219,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -250,6 +253,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -281,8 +286,9 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211104170005-ce137452f963 h1:8gJUadZl+kWvZBqG/LautX0X6qe5qTC2VI/3V3NBRAY=
golang.org/x/net v0.0.0-20211104170005-ce137452f963/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -302,6 +308,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -312,7 +319,6 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -335,12 +341,13 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -348,8 +355,10 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -405,6 +414,8 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+3 -1
View File
@@ -9,7 +9,9 @@
"base": "{{ .Base }}",
"version": "{{ .Version }}",
"authorizationNeeded": "{{ .AuthorizationNeeded }}",
"secured": "{{ .Secured }}"
"secured": "{{ .Secured }}",
"hostname": "{{ .Hostname }}",
"hosts": "{{ .Hosts }}"
}
</script>
<link
+28 -15
View File
@@ -37,8 +37,8 @@ func (s *DockerSecret) UnmarshalText(b []byte) error {
type args struct {
Addr string `arg:"env:DOZZLE_ADDR" default:":8080" help:"sets host:port to bind for server. This is rarely needed inside a docker container."`
Base string `arg:"env:DOZZLE_BASE" default:"/" help:"sets the base for http router."`
Hostname string `arg:"env:DOZZLE_HOSTNAME" help:"sets the hostname for display. This is useful with multiple Dozzle instances."`
Level string `arg:"env:DOZZLE_LEVEL" default:"info" help:"set Dozzle log level. Use debug for more logging."`
TailSize int `arg:"env:DOZZLE_TAILSIZE" default:"300" help:"update the initial tail size when fetching logs."`
Username string `arg:"env:DOZZLE_USERNAME" help:"sets the username for auth."`
Password string `arg:"env:DOZZLE_PASSWORD" help:"sets password for auth"`
UsernameFile *DockerSecret `arg:"env:DOZZLE_USERNAME_FILE" help:"sets the secret path read username for auth."`
@@ -48,6 +48,7 @@ type args struct {
FilterStrings []string `arg:"env:DOZZLE_FILTER,--filter,separate" help:"filters docker containers using Docker syntax."`
Filter map[string][]string `arg:"-"`
Healthcheck *HealthcheckCmd `arg:"subcommand:healthcheck" help:"checks if the server is running."`
RemoteHost []string `arg:"env:DOZZLE_REMOTE_HOST,--remote-host,separate" help:"list of hosts to connect remotely"`
}
type HealthcheckCmd struct {
@@ -91,6 +92,7 @@ func main() {
}
log.Infof("Dozzle version %s", version)
dockerClient := docker.NewClientWithFilters(args.Filter)
for i := 1; ; i++ {
_, err := dockerClient.ListContainers()
@@ -105,6 +107,15 @@ func main() {
}
}
clients := make(map[string]docker.Client)
clients["localhost"] = dockerClient
for _, host := range args.RemoteHost {
log.Infof("Creating client for %s", host)
client := docker.NewClientWithTlsAndFilter(args.Filter, host)
clients[host] = client
}
if args.Username == "" && args.UsernameFile != nil {
args.Username = args.UsernameFile.Value
}
@@ -120,12 +131,13 @@ func main() {
}
config := web.Config{
Addr: args.Addr,
Base: args.Base,
Version: version,
TailSize: args.TailSize,
Username: args.Username,
Password: args.Password,
Addr: args.Addr,
Base: args.Base,
Version: version,
Username: args.Username,
Password: args.Password,
Hostname: args.Hostname,
NoAnalytics: args.NoAnalytics,
}
assets, err := fs.Sub(content, "dist")
@@ -138,7 +150,7 @@ func main() {
assets = os.DirFS("./dist")
}
srv := web.CreateServer(dockerClient, assets, config)
srv := web.CreateServer(clients, assets, config)
go doStartEvent(args)
go func() {
log.Infof("Accepting connections on %s", srv.Addr)
@@ -170,13 +182,14 @@ func doStartEvent(arg args) {
}
event := analytics.StartEvent{
ClientId: host,
Version: version,
FilterLength: len(arg.Filter),
CustomAddress: arg.Addr != ":8080",
CustomBase: arg.Base != "/",
TailSize: arg.TailSize,
Protected: arg.Username != "",
ClientId: host,
Version: version,
FilterLength: len(arg.Filter),
CustomAddress: arg.Addr != ":8080",
CustomBase: arg.Base != "/",
RemoteHostLength: len(arg.RemoteHost),
Protected: arg.Username != "",
HasHostname: arg.Hostname != "",
}
if err := analytics.SendStartEvent(event); err != nil {
Executable
+3
View File
@@ -0,0 +1,3 @@
[build]
publish = "docs/.vitepress/dist"
command = "pnpm docs:build"
+105 -99
View File
@@ -1,103 +1,109 @@
{
"name": "dozzle",
"version": "4.5.0",
"description": "Realtime log viewer for docker containers. ",
"homepage": "https://github.com/amir20/dozzle#readme",
"bugs": {
"url": "https://github.com/amir20/dozzle/issues"
"name": "dozzle",
"version": "4.8.2",
"description": "Realtime log viewer for docker containers. ",
"homepage": "https://github.com/amir20/dozzle#readme",
"bugs": {
"url": "https://github.com/amir20/dozzle/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/amir20/dozzle.git"
},
"license": "ISC",
"author": "Amir Raminfar <findamir@gmail.com>",
"scripts": {
"watch:assets": "vite --open",
"watch:server": "LIVE_FS=true DOZZLE_ADDR=localhost:3100 reflex -c .reflex",
"dev": "make fake_assets && npm-run-all -p watch:assets watch:server",
"build": "vite build",
"release": "release-it",
"test": "TZ=UTC vitest",
"docs:dev": "vitepress dev docs --open",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
},
"dependencies": {
"@iconify-json/carbon": "^1.1.16",
"@iconify-json/cil": "^1.1.4",
"@iconify-json/mdi": "^1.1.50",
"@iconify-json/mdi-light": "^1.1.6",
"@iconify-json/octicon": "^1.1.36",
"@intlify/unplugin-vue-i18n": "^0.10.0",
"@oruga-ui/oruga-next": "^0.5.10",
"@oruga-ui/theme-bulma": "^0.2.9",
"@vueuse/core": "^9.13.0",
"@vueuse/integrations": "^9.13.0",
"@vueuse/router": "^9.13.0",
"ansi-to-html": "^0.7.2",
"bulma": "^0.9.4",
"d3-array": "^3.2.3",
"d3-ease": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-selection": "^3.0.0",
"d3-shape": "^3.2.0",
"d3-transition": "^3.0.1",
"date-fns": "^2.29.3",
"fuse.js": "^6.6.2",
"lodash.debounce": "^4.0.8",
"pinia": "^2.0.33",
"semver": "^7.3.8",
"splitpanes": "^3.1.5",
"vue": "^3.2.47",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@pinia/testing": "^0.0.15",
"@types/d3-array": "^3.0.4",
"@types/d3-ease": "^3.0.0",
"@types/d3-scale": "^4.0.3",
"@types/d3-selection": "^3.0.5",
"@types/d3-shape": "^3.1.1",
"@types/d3-transition": "^3.0.3",
"@types/lodash.debounce": "^4.0.7",
"@types/node": "^18.15.5",
"@types/semver": "^7.3.13",
"@vitejs/plugin-vue": "4.1.0",
"@vue/compiler-sfc": "^3.2.47",
"@vue/test-utils": "^2.3.2",
"c8": "^7.13.0",
"eventsourcemock": "^2.0.0",
"jest-serializer-vue": "^3.1.0",
"jsdom": "^21.1.1",
"lint-staged": "^13.2.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.8.6",
"release-it": "^15.9.1",
"sass": "^1.59.3",
"simple-git-hooks": "^2.8.1",
"ts-node": "^10.9.1",
"typescript": "^5.0.2",
"unplugin-auto-import": "^0.15.2",
"unplugin-icons": "^0.15.3",
"unplugin-vue-components": "^0.24.1",
"vite": "4.2.1",
"vite-plugin-pages": "^0.29.0",
"vite-plugin-vue-layouts": "^0.8.0",
"vitepress": "1.0.0-alpha.61",
"vitest": "^0.29.7",
"vue-tsc": "^1.2.0"
},
"lint-staged": {
"*.{js,vue,css,ts,html,md}": [
"prettier --write"
]
},
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged"
},
"release-it": {
"github": {
"release": false,
"releaseNotes": "git log --pretty=format:\"* %s (%h)\" $(git describe --abbrev=0 --tags $(git rev-list --tags --skip=1 --max-count=1))...HEAD~1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/amir20/dozzle.git"
},
"license": "ISC",
"author": "Amir Raminfar <findamir@gmail.com>",
"scripts": {
"watch:assets": "vite --open",
"watch:server": "LIVE_FS=true DOZZLE_ADDR=:3100 reflex -c .reflex",
"dev": "make fake_assets && npm-run-all -p watch:assets watch:server",
"build": "vite build",
"release": "release-it",
"test": "TZ=UTC vitest",
"postinstall": "husky install"
},
"dependencies": {
"@iconify-json/carbon": "^1.1.11",
"@iconify-json/cil": "^1.1.3",
"@iconify-json/mdi": "^1.1.36",
"@iconify-json/mdi-light": "^1.1.3",
"@iconify-json/octicon": "^1.1.24",
"@oruga-ui/oruga-next": "^0.5.9",
"@oruga-ui/theme-bulma": "^0.2.8",
"@vueuse/core": "^9.6.0",
"@vueuse/integrations": "^9.6.0",
"@vueuse/router": "^9.6.0",
"ansi-to-html": "^0.7.2",
"bulma": "^0.9.4",
"d3-array": "^3.2.1",
"d3-ease": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-selection": "^3.0.0",
"d3-shape": "^3.1.0",
"d3-transition": "^3.0.1",
"date-fns": "^2.29.3",
"fuse.js": "^6.6.2",
"lodash.debounce": "^4.0.8",
"pinia": "^2.0.28",
"semver": "^7.3.8",
"splitpanes": "^3.1.5",
"vue": "^3.2.45",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@intlify/vite-plugin-vue-i18n": "^6.0.3",
"@pinia/testing": "^0.0.14",
"@types/d3-array": "^3.0.3",
"@types/d3-ease": "^3.0.0",
"@types/d3-scale": "^4.0.2",
"@types/d3-selection": "^3.0.3",
"@types/d3-shape": "^3.1.0",
"@types/d3-transition": "^3.0.2",
"@types/lodash.debounce": "^4.0.7",
"@types/node": "^18.11.12",
"@types/semver": "^7.3.13",
"@vitejs/plugin-vue": "4.0.0",
"@vue/compiler-sfc": "^3.2.45",
"@vue/test-utils": "^2.2.6",
"c8": "^7.12.0",
"eventsourcemock": "^2.0.0",
"husky": "^8.0.2",
"jest-serializer-vue": "^3.0.0",
"jsdom": "^20.0.3",
"lint-staged": "^13.1.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.8.1",
"release-it": "^15.5.1",
"sass": "^1.56.2",
"ts-node": "^10.9.1",
"typescript": "^4.9.4",
"unplugin-auto-import": "^0.12.0",
"unplugin-icons": "^0.14.14",
"unplugin-vue-components": "^0.22.11",
"vite": "4.0.0",
"vite-plugin-pages": "^0.27.1",
"vite-plugin-vue-layouts": "^0.7.0",
"vitest": "^0.25.6",
"vue-tsc": "^1.0.12"
},
"lint-staged": {
"*.{js,vue,css}": [
"prettier --write"
]
},
"release-it": {
"github": {
"release": false,
"releaseNotes": "git log --pretty=format:\"* %s (%h)\" $(git describe --abbrev=0 --tags $(git rev-list --tags --skip=1 --max-count=1))...HEAD~1"
},
"npm": {
"publish": false
}
"npm": {
"publish": false
}
}
}
+1526 -1300
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
packages:
- docs
- e2e
+13 -3
View File
@@ -7,7 +7,7 @@ import AutoImport from "unplugin-auto-import/vite";
import IconsResolver from "unplugin-icons/resolver";
import Pages from "vite-plugin-pages";
import Layouts from "vite-plugin-vue-layouts";
import VueI18n from "@intlify/vite-plugin-vue-i18n";
import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite";
export default defineConfig(() => ({
resolve: {
@@ -26,6 +26,11 @@ export default defineConfig(() => ({
plugins: [
vue({
reactivityTransform: true,
template: {
compilerOptions: {
whitespace: "preserve",
},
},
}),
Icons({
autoInstall: true,
@@ -53,16 +58,21 @@ export default defineConfig(() => ({
dirs: ["assets/composables", "assets/stores", "assets/utils"],
vueTemplate: true,
}),
VueI18n({
VueI18nPlugin({
runtimeOnly: true,
compositionOnly: true,
strictMessage: false,
include: [path.resolve(__dirname, "locales/**")],
}),
],
server: {
proxy: {
"/api": {
target: "http://localhost:3100/",
target: {
host: "127.0.0.1",
port: 3100,
},
changeOrigin: false,
},
},
},
+10 -2
View File
@@ -71,6 +71,14 @@ Content-Type: text/html
<pre>dev</pre>
/* snapshot: Test_handler_between_dates */
HTTP/1.1 200 OK
Connection: close
Content-Type: application/ld+json; charset=UTF-8
{"m":"INFO Testing logs...","ts":1589396137772,"id":2908612274,"l":"info"}
{"m":"INFO Testing logs...","ts":1589396137772,"id":2908612274,"l":"info"}
/* snapshot: Test_handler_streamEvents_error */
HTTP/1.1 200 OK
Connection: close
@@ -144,7 +152,7 @@ Connection: keep-alive
Content-Type: text/event-stream
X-Accel-Buffering: no
data: {"m":"INFO Testing logs...","ts":0,"id":4256192898}
data: {"m":"INFO Testing logs...","ts":0,"id":4256192898,"l":"info"}
event: container-stopped
data: end of stream
@@ -170,7 +178,7 @@ Connection: keep-alive
Content-Type: text/event-stream
X-Accel-Buffering: no
data: {"m":"INFO Testing logs...","ts":1589396137772,"id":1469707724}
data: {"m":"INFO Testing logs...","ts":1589396137772,"id":1469707724,"l":"info"}
id: 1589396137772
event: container-stopped
+19 -15
View File
@@ -1,7 +1,9 @@
package web
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
@@ -25,25 +27,27 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
events, err := h.client.Events(ctx)
client := h.clientFromRequest(r)
events, err := client.Events(ctx)
stats := make(chan docker.ContainerStat)
if containers, err := h.client.ListContainers(); err == nil {
for _, c := range containers {
if c.State == "running" {
if err := h.client.ContainerStats(ctx, c.ID, stats); err != nil {
log.Errorf("error while streaming container stats: %v", err)
}
}
}
}
if err := sendContainersJSON(h.client, w); err != nil {
if err := sendContainersJSON(client, w); err != nil {
log.Errorf("error while encoding containers to stream: %v", err)
}
f.Flush()
if containers, err := client.ListContainers(); err == nil {
go func() {
for _, c := range containers {
if c.State == "running" {
if err := client.ContainerStats(ctx, c.ID, stats); err != nil && !errors.Is(err, context.Canceled) {
log.Errorf("error while streaming container stats: %v", err)
}
}
}
}()
}
for {
select {
case stat := <-stats:
@@ -62,10 +66,10 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
log.Debugf("triggering docker event: %v", event.Name)
if event.Name == "start" {
log.Debugf("found new container with id: %v", event.ActorID)
if err := h.client.ContainerStats(ctx, event.ActorID, stats); err != nil {
if err := client.ContainerStats(ctx, event.ActorID, stats); err != nil && !errors.Is(err, context.Canceled) {
log.Errorf("error when streaming new container stats: %v", err)
}
if err := sendContainersJSON(h.client, w); err != nil {
if err := sendContainersJSON(client, w); err != nil {
log.Errorf("error encoding containers to stream: %v", err)
return
}
+17 -49
View File
@@ -5,13 +5,11 @@ import (
"compress/gzip"
"context"
"encoding/json"
"hash/fnv"
"fmt"
"io"
"net/http"
"runtime"
"strings"
"time"
@@ -23,7 +21,7 @@ import (
func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
container, err := h.client.FindContainer(id)
container, err := h.clientFromRequest(r).FindContainer(id)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@@ -40,7 +38,7 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
zw.Comment = "Logs generated by Dozzle"
zw.ModTime = now
reader, err := h.client.ContainerLogsBetweenDates(r.Context(), container.ID, from, now)
reader, err := h.clientFromRequest(r).ContainerLogsBetweenDates(r.Context(), container.ID, from, now)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -48,36 +46,6 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
io.Copy(zw, reader)
}
func logEventIterator(reader *bufio.Reader) func() (docker.LogEvent, error) {
return func() (docker.LogEvent, error) {
message, readerError := reader.ReadString('\n')
h := fnv.New32a()
h.Write([]byte(message))
logEvent := docker.LogEvent{Id: h.Sum32(), Message: message}
if index := strings.IndexAny(message, " "); index != -1 {
logId := message[:index]
if timestamp, err := time.Parse(time.RFC3339Nano, logId); err == nil {
logEvent.Timestamp = timestamp.UnixMilli()
message = strings.TrimSuffix(message[index+1:], "\n")
logEvent.Message = message
if strings.HasPrefix(message, "{") && strings.HasSuffix(message, "}") {
var data map[string]interface{}
if err := json.Unmarshal([]byte(message), &data); err != nil {
log.Errorf("json unmarshal error while streaming %v", err.Error())
} else {
logEvent.Message = data
}
}
}
}
return logEvent, readerError
}
}
func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/ld+json; charset=UTF-8")
@@ -85,7 +53,7 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
to, _ := time.Parse(time.RFC3339, r.URL.Query().Get("to"))
id := r.URL.Query().Get("id")
reader, err := h.client.ContainerLogsBetweenDates(r.Context(), id, from, to)
reader, err := h.clientFromRequest(r).ContainerLogsBetweenDates(r.Context(), id, from, to)
defer reader.Close()
if err != nil {
@@ -94,13 +62,14 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
}
buffered := bufio.NewReader(reader)
eventIterator := logEventIterator(buffered)
iterator := docker.NewEventIterator(buffered)
for {
logEvent, readerError := eventIterator()
logEvent, readerError := iterator.Next()
if readerError != nil {
break
}
if err := json.NewEncoder(w).Encode(logEvent); err != nil {
log.Errorf("json encoding error while streaming %v", err.Error())
}
@@ -120,7 +89,7 @@ func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
return
}
container, err := h.client.FindContainer(id)
container, err := h.clientFromRequest(r).FindContainer(id)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
@@ -137,7 +106,7 @@ func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
lastEventId = r.URL.Query().Get("lastEventId")
}
reader, err := h.client.ContainerLogs(r.Context(), container.ID, h.config.TailSize, lastEventId)
reader, err := h.clientFromRequest(r).ContainerLogs(r.Context(), container.ID, lastEventId)
if err != nil {
if err == io.EOF {
fmt.Fprintf(w, "event: container-stopped\ndata: end of stream\n\n")
@@ -165,13 +134,15 @@ func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
}()
buffered := bufio.NewReader(reader)
var readerError error
eventIterator := logEventIterator(buffered)
iterator := docker.NewEventIterator(buffered)
for {
var logEvent docker.LogEvent
logEvent, readerError = eventIterator()
logEvent, err := iterator.Next()
if err != nil {
break
}
if buf, err := json.Marshal(logEvent); err != nil {
log.Errorf("json encoding error while streaming %v", err.Error())
} else {
@@ -182,19 +153,16 @@ func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
}
fmt.Fprintf(w, "\n")
f.Flush()
if readerError != nil {
break
}
}
log.Debugf("streaming stopped: %v", container.ID)
if readerError == io.EOF {
if iterator.LastError() == io.EOF {
log.Debugf("container stopped: %v", container.ID)
fmt.Fprintf(w, "event: container-stopped\ndata: end of stream\n\n")
f.Flush()
} else if readerError != context.Canceled {
log.Errorf("unknown error while streaming %v", readerError.Error())
} else if iterator.LastError() != context.Canceled {
log.Errorf("unknown error while streaming %v", iterator.LastError().Error())
}
log.WithField("routines", runtime.NumGoroutine()).Debug("runtime goroutine stats")
+54 -10
View File
@@ -6,8 +6,11 @@ import (
"io/fs"
"io/ioutil"
"net/http"
"os"
"path"
"strings"
"github.com/amir20/dozzle/analytics"
"github.com/amir20/dozzle/docker"
"github.com/gorilla/mux"
@@ -16,24 +19,25 @@ import (
// Config is a struct for configuring the web service
type Config struct {
Base string
Addr string
Version string
TailSize int
Username string
Password string
Base string
Addr string
Version string
Username string
Password string
Hostname string
NoAnalytics bool
}
type handler struct {
client docker.Client
clients map[string]docker.Client
content fs.FS
config *Config
}
// CreateServer creates a service for http handler
func CreateServer(c docker.Client, content fs.FS, config Config) *http.Server {
func CreateServer(clients map[string]docker.Client, content fs.FS, config Config) *http.Server {
handler := &handler{
client: c,
clients: clients,
content: content,
config: &config,
}
@@ -78,6 +82,28 @@ func (h *handler) index(w http.ResponseWriter, req *http.Request) {
_, err := h.content.Open(req.URL.Path)
if err == nil && req.URL.Path != "" && req.URL.Path != "/" {
fileServer.ServeHTTP(w, req)
if !h.config.NoAnalytics {
go func() {
host, _ := os.Hostname()
if containers, err := h.clients["localhost"].ListContainers(); err == nil {
totalContainers := len(containers)
runningContainers := 0
for _, container := range containers {
if container.State == "running" {
runningContainers++
}
}
re := analytics.RequestEvent{
ClientId: host,
TotalContainers: totalContainers,
RunningContainers: runningContainers,
}
analytics.SendRequestEvent(re)
}
}()
}
} else {
if !isAuthorized(req) && req.URL.Path != "login" {
http.Redirect(w, req, path.Clean(h.config.Base+"/login"), http.StatusTemporaryRedirect)
@@ -106,16 +132,26 @@ func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) {
path = h.config.Base
}
// Get all keys from hosts map
hosts := make([]string, 0, len(h.clients))
for k := range h.clients {
hosts = append(hosts, k)
}
data := struct {
Base string
Version string
AuthorizationNeeded bool
Secured bool
Hostname string
Hosts string
}{
path,
h.config.Version,
h.isAuthorizationNeeded(req),
secured,
h.config.Hostname,
strings.Join(hosts, ","),
}
err = tmpl.Execute(w, data)
if err != nil {
@@ -132,10 +168,18 @@ func (h *handler) version(w http.ResponseWriter, r *http.Request) {
func (h *handler) healthcheck(w http.ResponseWriter, r *http.Request) {
log.Trace("Executing healthcheck request")
if ping, err := h.client.Ping(r.Context()); err != nil {
if ping, err := h.clients["localhost"].Ping(r.Context()); err != nil {
log.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
} else {
fmt.Fprintf(w, "OK API Version %v", ping.APIVersion)
}
}
func (h *handler) clientFromRequest(r *http.Request) docker.Client {
host := r.URL.Query().Get("host")
if client, ok := h.clients[host]; ok {
return client
}
return h.clients["localhost"]
}
+214
View File
@@ -0,0 +1,214 @@
package web
import (
"bytes"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/magiconair/properties/assert"
"github.com/amir20/dozzle/docker"
"github.com/beme/abide"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/spf13/afero"
)
func Test_createRoutes_index(t *testing.T) {
fs := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.")
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/"})
req, err := http.NewRequest("GET", "/", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
}
func Test_createRoutes_redirect(t *testing.T) {
fs := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.")
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/foobar"})
req, err := http.NewRequest("GET", "/foobar", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
}
func Test_createRoutes_redirect_with_auth(t *testing.T) {
fs := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.")
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/foobar", Username: "amir", Password: "password"})
req, err := http.NewRequest("GET", "/foobar/", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
}
func Test_createRoutes_foobar(t *testing.T) {
fs := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fs, "index.html", []byte("foo page"), 0644), "WriteFile should have no error.")
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/foobar"})
req, err := http.NewRequest("GET", "/foobar/", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
}
func Test_createRoutes_foobar_file(t *testing.T) {
fs := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.")
require.NoError(t, afero.WriteFile(fs, "test", []byte("test page"), 0644), "WriteFile should have no error.")
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/foobar"})
req, err := http.NewRequest("GET", "/foobar/test", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, rr.Body.String(), "test page", "page doesn't match")
}
func Test_createRoutes_version(t *testing.T) {
fs := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.")
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/", Version: "dev"})
req, err := http.NewRequest("GET", "/version", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
}
func Test_createRoutes_username_password(t *testing.T) {
handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password"})
req, err := http.NewRequest("GET", "/", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
}
func Test_createRoutes_username_password_invalid(t *testing.T) {
handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password"})
req, err := http.NewRequest("GET", "/api/logs/stream?id=123", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
}
func Test_createRoutes_username_password_login_happy(t *testing.T) {
handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password"})
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
fw, err := writer.CreateFormField("username")
require.NoError(t, err, "Creating field should not be error.")
_, err = io.Copy(fw, strings.NewReader("amir"))
require.NoError(t, err, "Copying field should not result in error.")
fw, err = writer.CreateFormField("password")
require.NoError(t, err, "Creating field should not be error.")
_, err = io.Copy(fw, strings.NewReader("password"))
require.NoError(t, err, "Copying field should not result in error.")
writer.Close()
req, err := http.NewRequest("POST", "/api/validateCredentials", bytes.NewReader(body.Bytes()))
req.Header.Set("Content-Type", writer.FormDataContentType())
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, rr.Code, 200)
cookie := rr.Header().Get("Set-Cookie")
assert.Matches(t, cookie, "session=.+")
}
func Test_createRoutes_username_password_login_failed(t *testing.T) {
handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password"})
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
fw, err := writer.CreateFormField("username")
require.NoError(t, err, "Creating field should not be error.")
_, err = io.Copy(fw, strings.NewReader("amir"))
require.NoError(t, err, "Copying field should not result in error.")
fw, err = writer.CreateFormField("password")
require.NoError(t, err, "Creating field should not be error.")
_, err = io.Copy(fw, strings.NewReader("bad"))
require.NoError(t, err, "Copying field should not result in error.")
writer.Close()
req, err := http.NewRequest("POST", "/api/validateCredentials", bytes.NewReader(body.Bytes()))
req.Header.Set("Content-Type", writer.FormDataContentType())
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, rr.Code, 401)
}
func Test_createRoutes_username_password_valid_session(t *testing.T) {
mockedClient := new(MockedClient)
mockedClient.On("FindContainer", "123").Return(docker.Container{ID: "123"}, nil)
mockedClient.On("ContainerLogs", mock.Anything, "123", "").Return(ioutil.NopCloser(strings.NewReader("test data")), io.EOF)
handler := createHandler(mockedClient, nil, Config{Base: "/", Username: "amir", Password: "password"})
// Get cookie first
req, err := http.NewRequest("GET", "/api/logs/stream?id=123", nil)
require.NoError(t, err, "NewRequest should not return an error.")
session, _ := store.Get(req, sessionName)
session.Values[authorityKey] = time.Now().Unix()
recorder := httptest.NewRecorder()
session.Save(req, recorder)
cookies := recorder.Result().Cookies()
// Test with cookie
req, err = http.NewRequest("GET", "/api/logs/stream?id=123", nil)
require.NoError(t, err, "NewRequest should not return an error.")
req.AddCookie(cookies[0])
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
}
func Test_createRoutes_username_password_invalid_session(t *testing.T) {
mockedClient := new(MockedClient)
mockedClient.On("FindContainer", "123").Return(docker.Container{ID: "123"}, nil)
mockedClient.On("ContainerLogs", mock.Anything, "since").Return(ioutil.NopCloser(strings.NewReader("test data")), io.EOF)
handler := createHandler(mockedClient, nil, Config{Base: "/", Username: "amir", Password: "password"})
req, err := http.NewRequest("GET", "/api/logs/stream?id=123", nil)
require.NoError(t, err, "NewRequest should not return an error.")
req.AddCookie(&http.Cookie{Name: "session", Value: "baddata"})
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, rr.Code, 401)
}
+250
View File
@@ -0,0 +1,250 @@
package web
import (
"context"
"errors"
"io"
"time"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/amir20/dozzle/docker"
"github.com/beme/abide"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func Test_handler_streamLogs_happy(t *testing.T) {
id := "123456"
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
q := req.URL.Query()
q.Add("id", id)
req.URL.RawQuery = q.Encode()
require.NoError(t, err, "NewRequest should not return an error.")
mockedClient := new(MockedClient)
reader := ioutil.NopCloser(strings.NewReader("INFO Testing logs..."))
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil)
mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, "").Return(reader, nil)
clients := map[string]docker.Client{
"localhost": mockedClient,
}
h := handler{clients: clients, config: &Config{}}
handler := http.HandlerFunc(h.streamLogs)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
func Test_handler_streamLogs_happy_with_id(t *testing.T) {
id := "123456"
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
q := req.URL.Query()
q.Add("id", id)
req.URL.RawQuery = q.Encode()
require.NoError(t, err, "NewRequest should not return an error.")
mockedClient := new(MockedClient)
reader := ioutil.NopCloser(strings.NewReader("2020-05-13T18:55:37.772853839Z INFO Testing logs..."))
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil)
mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, "").Return(reader, nil)
clients := map[string]docker.Client{
"localhost": mockedClient,
}
h := handler{clients: clients, config: &Config{}}
handler := http.HandlerFunc(h.streamLogs)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
func Test_handler_streamLogs_happy_container_stopped(t *testing.T) {
id := "123456"
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
q := req.URL.Query()
q.Add("id", id)
req.URL.RawQuery = q.Encode()
require.NoError(t, err, "NewRequest should not return an error.")
mockedClient := new(MockedClient)
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil)
mockedClient.On("ContainerLogs", mock.Anything, id, "").Return(ioutil.NopCloser(strings.NewReader("")), io.EOF)
clients := map[string]docker.Client{
"localhost": mockedClient,
}
h := handler{clients: clients, config: &Config{}}
handler := http.HandlerFunc(h.streamLogs)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
func Test_handler_streamLogs_error_finding_container(t *testing.T) {
id := "123456"
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
q := req.URL.Query()
q.Add("id", id)
req.URL.RawQuery = q.Encode()
require.NoError(t, err, "NewRequest should not return an error.")
mockedClient := new(MockedClient)
mockedClient.On("FindContainer", id).Return(docker.Container{}, errors.New("error finding container"))
clients := map[string]docker.Client{
"localhost": mockedClient,
}
h := handler{clients: clients, config: &Config{}}
handler := http.HandlerFunc(h.streamLogs)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
func Test_handler_streamLogs_error_reading(t *testing.T) {
id := "123456"
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
q := req.URL.Query()
q.Add("id", id)
req.URL.RawQuery = q.Encode()
require.NoError(t, err, "NewRequest should not return an error.")
mockedClient := new(MockedClient)
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil)
mockedClient.On("ContainerLogs", mock.Anything, id, "").Return(ioutil.NopCloser(strings.NewReader("")), errors.New("test error"))
clients := map[string]docker.Client{
"localhost": mockedClient,
}
h := handler{clients: clients, config: &Config{}}
handler := http.HandlerFunc(h.streamLogs)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
func Test_handler_streamEvents_happy(t *testing.T) {
req, err := http.NewRequest("GET", "/api/events/stream", nil)
require.NoError(t, err, "NewRequest should not return an error.")
mockedClient := new(MockedClient)
messages := make(chan docker.ContainerEvent)
errChannel := make(chan error)
mockedClient.On("Events", mock.Anything).Return(messages, errChannel)
mockedClient.On("ListContainers").Return([]docker.Container{}, nil)
go func() {
messages <- docker.ContainerEvent{
Name: "start",
ActorID: "1234",
}
messages <- docker.ContainerEvent{
Name: "something-random",
ActorID: "1234",
}
close(messages)
}()
clients := map[string]docker.Client{
"localhost": mockedClient,
}
h := handler{clients: clients, config: &Config{}}
handler := http.HandlerFunc(h.streamEvents)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
func Test_handler_streamEvents_error(t *testing.T) {
req, err := http.NewRequest("GET", "/api/events/stream", nil)
require.NoError(t, err, "NewRequest should not return an error.")
mockedClient := new(MockedClient)
messages := make(chan docker.ContainerEvent)
errChannel := make(chan error)
mockedClient.On("Events", mock.Anything).Return(messages, errChannel)
mockedClient.On("ListContainers").Return([]docker.Container{}, nil)
go func() {
errChannel <- errors.New("fake error")
close(messages)
}()
clients := map[string]docker.Client{
"localhost": mockedClient,
}
h := handler{clients: clients, config: &Config{}}
handler := http.HandlerFunc(h.streamEvents)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
func Test_handler_streamEvents_error_request(t *testing.T) {
req, err := http.NewRequest("GET", "/api/events/stream", nil)
require.NoError(t, err, "NewRequest should not return an error.")
mockedClient := new(MockedClient)
messages := make(chan docker.ContainerEvent)
errChannel := make(chan error)
mockedClient.On("Events", mock.Anything).Return(messages, errChannel)
mockedClient.On("ListContainers").Return([]docker.Container{}, nil)
ctx, cancel := context.WithCancel(context.Background())
req = req.WithContext(ctx)
go func() {
cancel()
}()
clients := map[string]docker.Client{
"localhost": mockedClient,
}
h := handler{clients: clients, config: &Config{}}
handler := http.HandlerFunc(h.streamEvents)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
// for /api/logs
func Test_handler_between_dates(t *testing.T) {
req, err := http.NewRequest("GET", "/api/logs", nil)
require.NoError(t, err, "NewRequest should not return an error.")
from, _ := time.Parse(time.RFC3339, "2018-01-01T00:00:00Z")
to, _ := time.Parse(time.RFC3339, "2018-01-01T010:00:00Z")
q := req.URL.Query()
q.Add("from", from.Format(time.RFC3339))
q.Add("to", to.Format(time.RFC3339))
q.Add("id", "123456")
req.URL.RawQuery = q.Encode()
mockedClient := new(MockedClient)
reader := ioutil.NopCloser(strings.NewReader("2020-05-13T18:55:37.772853839Z INFO Testing logs...\n2020-05-13T18:55:37.772853839Z INFO Testing logs...\n"))
mockedClient.On("ContainerLogsBetweenDates", mock.Anything, "123456", from, to).Return(reader, nil)
clients := map[string]docker.Client{
"localhost": mockedClient,
}
h := handler{clients: clients, config: &Config{}}
handler := http.HandlerFunc(h.fetchLogsBetweenDates)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
+14 -388
View File
@@ -1,27 +1,17 @@
package web
import (
"bytes"
"context"
"errors"
"io"
"io/fs"
"io/ioutil"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"io"
"io/fs"
"github.com/gorilla/mux"
"github.com/magiconair/properties/assert"
"github.com/amir20/dozzle/docker"
"github.com/beme/abide"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/spf13/afero"
)
@@ -41,8 +31,8 @@ func (m *MockedClient) ListContainers() ([]docker.Container, error) {
return args.Get(0).([]docker.Container), args.Error(1)
}
func (m *MockedClient) ContainerLogs(ctx context.Context, id string, tailSize int, since string) (io.ReadCloser, error) {
args := m.Called(ctx, id, tailSize)
func (m *MockedClient) ContainerLogs(ctx context.Context, id string, since string) (io.ReadCloser, error) {
args := m.Called(ctx, id, since)
return args.Get(0).(io.ReadCloser), args.Error(1)
}
@@ -64,376 +54,15 @@ func (m *MockedClient) ContainerStats(context.Context, string, chan<- docker.Con
return nil
}
func Test_handler_streamLogs_happy(t *testing.T) {
id := "123456"
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
q := req.URL.Query()
q.Add("id", id)
req.URL.RawQuery = q.Encode()
require.NoError(t, err, "NewRequest should not return an error.")
mockedClient := new(MockedClient)
reader := ioutil.NopCloser(strings.NewReader("INFO Testing logs..."))
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil)
mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, 300).Return(reader, nil)
h := handler{client: mockedClient, config: &Config{TailSize: 300}}
handler := http.HandlerFunc(h.streamLogs)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
func Test_handler_streamLogs_happy_with_id(t *testing.T) {
id := "123456"
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
q := req.URL.Query()
q.Add("id", id)
req.URL.RawQuery = q.Encode()
require.NoError(t, err, "NewRequest should not return an error.")
mockedClient := new(MockedClient)
reader := ioutil.NopCloser(strings.NewReader("2020-05-13T18:55:37.772853839Z INFO Testing logs..."))
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil)
mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, 300).Return(reader, nil)
h := handler{client: mockedClient, config: &Config{TailSize: 300}}
handler := http.HandlerFunc(h.streamLogs)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
func Test_handler_streamLogs_happy_container_stopped(t *testing.T) {
id := "123456"
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
q := req.URL.Query()
q.Add("id", id)
req.URL.RawQuery = q.Encode()
require.NoError(t, err, "NewRequest should not return an error.")
mockedClient := new(MockedClient)
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil)
mockedClient.On("ContainerLogs", mock.Anything, id, 300).Return(ioutil.NopCloser(strings.NewReader("")), io.EOF)
h := handler{client: mockedClient, config: &Config{TailSize: 300}}
handler := http.HandlerFunc(h.streamLogs)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
func Test_handler_streamLogs_error_finding_container(t *testing.T) {
id := "123456"
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
q := req.URL.Query()
q.Add("id", id)
req.URL.RawQuery = q.Encode()
require.NoError(t, err, "NewRequest should not return an error.")
mockedClient := new(MockedClient)
mockedClient.On("FindContainer", id).Return(docker.Container{}, errors.New("error finding container"))
h := handler{client: mockedClient, config: &Config{TailSize: 300}}
handler := http.HandlerFunc(h.streamLogs)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
func Test_handler_streamLogs_error_reading(t *testing.T) {
id := "123456"
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
q := req.URL.Query()
q.Add("id", id)
req.URL.RawQuery = q.Encode()
require.NoError(t, err, "NewRequest should not return an error.")
mockedClient := new(MockedClient)
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil)
mockedClient.On("ContainerLogs", mock.Anything, id, 300).Return(ioutil.NopCloser(strings.NewReader("")), errors.New("test error"))
h := handler{client: mockedClient, config: &Config{TailSize: 300}}
handler := http.HandlerFunc(h.streamLogs)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
func Test_handler_streamEvents_happy(t *testing.T) {
req, err := http.NewRequest("GET", "/api/events/stream", nil)
require.NoError(t, err, "NewRequest should not return an error.")
mockedClient := new(MockedClient)
messages := make(chan docker.ContainerEvent)
errChannel := make(chan error)
mockedClient.On("Events", mock.Anything).Return(messages, errChannel)
mockedClient.On("ListContainers").Return([]docker.Container{}, nil)
go func() {
messages <- docker.ContainerEvent{
Name: "start",
ActorID: "1234",
}
messages <- docker.ContainerEvent{
Name: "something-random",
ActorID: "1234",
}
close(messages)
}()
h := handler{client: mockedClient, config: &Config{TailSize: 300}}
handler := http.HandlerFunc(h.streamEvents)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
func Test_handler_streamEvents_error(t *testing.T) {
req, err := http.NewRequest("GET", "/api/events/stream", nil)
require.NoError(t, err, "NewRequest should not return an error.")
mockedClient := new(MockedClient)
messages := make(chan docker.ContainerEvent)
errChannel := make(chan error)
mockedClient.On("Events", mock.Anything).Return(messages, errChannel)
mockedClient.On("ListContainers").Return([]docker.Container{}, nil)
go func() {
errChannel <- errors.New("fake error")
close(messages)
}()
h := handler{client: mockedClient, config: &Config{TailSize: 300}}
handler := http.HandlerFunc(h.streamEvents)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
func Test_handler_streamEvents_error_request(t *testing.T) {
req, err := http.NewRequest("GET", "/api/events/stream", nil)
require.NoError(t, err, "NewRequest should not return an error.")
mockedClient := new(MockedClient)
messages := make(chan docker.ContainerEvent)
errChannel := make(chan error)
mockedClient.On("Events", mock.Anything).Return(messages, errChannel)
mockedClient.On("ListContainers").Return([]docker.Container{}, nil)
ctx, cancel := context.WithCancel(context.Background())
req = req.WithContext(ctx)
go func() {
cancel()
}()
h := handler{client: mockedClient, config: &Config{TailSize: 300}}
handler := http.HandlerFunc(h.streamEvents)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
func Test_createRoutes_index(t *testing.T) {
fs := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.")
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/"})
req, err := http.NewRequest("GET", "/", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
}
func Test_createRoutes_redirect(t *testing.T) {
fs := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.")
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/foobar"})
req, err := http.NewRequest("GET", "/foobar", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
}
func Test_createRoutes_redirect_with_auth(t *testing.T) {
fs := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.")
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/foobar", Username: "amir", Password: "password"})
req, err := http.NewRequest("GET", "/foobar/", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
}
func Test_createRoutes_foobar(t *testing.T) {
fs := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fs, "index.html", []byte("foo page"), 0644), "WriteFile should have no error.")
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/foobar"})
req, err := http.NewRequest("GET", "/foobar/", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
}
func Test_createRoutes_foobar_file(t *testing.T) {
fs := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.")
require.NoError(t, afero.WriteFile(fs, "test", []byte("test page"), 0644), "WriteFile should have no error.")
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/foobar"})
req, err := http.NewRequest("GET", "/foobar/test", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, rr.Body.String(), "test page", "page doesn't match")
}
func Test_createRoutes_version(t *testing.T) {
fs := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.")
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/", Version: "dev"})
req, err := http.NewRequest("GET", "/version", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
}
func Test_createRoutes_username_password(t *testing.T) {
handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password"})
req, err := http.NewRequest("GET", "/", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
}
func Test_createRoutes_username_password_invalid(t *testing.T) {
handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password"})
req, err := http.NewRequest("GET", "/api/logs/stream?id=123", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
}
func Test_createRoutes_username_password_login_happy(t *testing.T) {
handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password"})
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
fw, err := writer.CreateFormField("username")
require.NoError(t, err, "Creating field should not be error.")
_, err = io.Copy(fw, strings.NewReader("amir"))
require.NoError(t, err, "Copying field should not result in error.")
fw, err = writer.CreateFormField("password")
require.NoError(t, err, "Creating field should not be error.")
_, err = io.Copy(fw, strings.NewReader("password"))
require.NoError(t, err, "Copying field should not result in error.")
writer.Close()
req, err := http.NewRequest("POST", "/api/validateCredentials", bytes.NewReader(body.Bytes()))
req.Header.Set("Content-Type", writer.FormDataContentType())
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, rr.Code, 200)
cookie := rr.Header().Get("Set-Cookie")
assert.Matches(t, cookie, "session=.+")
}
func Test_createRoutes_username_password_login_failed(t *testing.T) {
handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password"})
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
fw, err := writer.CreateFormField("username")
require.NoError(t, err, "Creating field should not be error.")
_, err = io.Copy(fw, strings.NewReader("amir"))
require.NoError(t, err, "Copying field should not result in error.")
fw, err = writer.CreateFormField("password")
require.NoError(t, err, "Creating field should not be error.")
_, err = io.Copy(fw, strings.NewReader("bad"))
require.NoError(t, err, "Copying field should not result in error.")
writer.Close()
req, err := http.NewRequest("POST", "/api/validateCredentials", bytes.NewReader(body.Bytes()))
req.Header.Set("Content-Type", writer.FormDataContentType())
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, rr.Code, 401)
}
func Test_createRoutes_username_password_valid_session(t *testing.T) {
mockedClient := new(MockedClient)
mockedClient.On("FindContainer", "123").Return(docker.Container{ID: "123"}, nil)
mockedClient.On("ContainerLogs", mock.Anything, "123", 0).Return(ioutil.NopCloser(strings.NewReader("test data")), io.EOF)
handler := createHandler(mockedClient, nil, Config{Base: "/", Username: "amir", Password: "password"})
// Get cookie first
req, err := http.NewRequest("GET", "/api/logs/stream?id=123", nil)
require.NoError(t, err, "NewRequest should not return an error.")
session, _ := store.Get(req, sessionName)
session.Values[authorityKey] = time.Now().Unix()
recorder := httptest.NewRecorder()
session.Save(req, recorder)
cookies := recorder.Result().Cookies()
// Test with cookie
req, err = http.NewRequest("GET", "/api/logs/stream?id=123", nil)
require.NoError(t, err, "NewRequest should not return an error.")
req.AddCookie(cookies[0])
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
}
func Test_createRoutes_username_password_invalid_session(t *testing.T) {
mockedClient := new(MockedClient)
mockedClient.On("FindContainer", "123").Return(docker.Container{ID: "123"}, nil)
mockedClient.On("ContainerLogs", mock.Anything, "123", 0).Return(ioutil.NopCloser(strings.NewReader("test data")), io.EOF)
handler := createHandler(mockedClient, nil, Config{Base: "/", Username: "amir", Password: "password"})
req, err := http.NewRequest("GET", "/api/logs/stream?id=123", nil)
require.NoError(t, err, "NewRequest should not return an error.")
req.AddCookie(&http.Cookie{Name: "session", Value: "baddata"})
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, rr.Code, 401)
func (m *MockedClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time) (io.ReadCloser, error) {
args := m.Called(ctx, id, from, to)
return args.Get(0).(io.ReadCloser), args.Error(1)
}
func createHandler(client docker.Client, content fs.FS, config Config) *mux.Router {
if client == nil {
client = new(MockedClient)
client.(*MockedClient).On("ListContainers").Return([]docker.Container{}, nil)
}
if content == nil {
@@ -442,15 +71,12 @@ func createHandler(client docker.Client, content fs.FS, config Config) *mux.Rout
content = afero.NewIOFS(fs)
}
clients := map[string]docker.Client{
"localhost": client,
}
return createRouter(&handler{
client: client,
clients: clients,
content: content,
config: &config,
})
}
func TestMain(m *testing.M) {
exit := m.Run()
abide.Cleanup()
os.Exit(exit)
}