Compare commits

...

161 Commits

Author SHA1 Message Date
Amir Raminfar
9259bf65ef Release 3.12.0 2022-04-17 09:40:47 -07:00
Amir Raminfar
8819c78487 Updates modules 2022-04-17 09:39:30 -07:00
Amir Raminfar
ba32d125ac Tries to inline favicon to fix #1714 (#1717)
* Tries to inline favicon to fix #1714

* Updates go tests
2022-04-16 14:55:34 -07:00
kodiakhq[bot]
6434c5341a Merge pull request #1713 from amir20/dependabot/docker/golang-1.18.1-alpine
Bump golang from 1.18.0-alpine to 1.18.1-alpine
2022-04-13 09:14:18 +00:00
dependabot[bot]
a12a4f2f79 Bump golang from 1.18.0-alpine to 1.18.1-alpine
Bumps golang from 1.18.0-alpine to 1.18.1-alpine.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-13 09:09:28 +00:00
Amir Raminfar
0eb379ce3d Updates modules 2022-04-08 20:14:16 -07:00
Clemens Wolff
76f83800f7 Add option to wait for docker before starting (#1705) 2022-04-02 20:30:21 -07:00
Clemens Wolff
f7d82d2ede Improve setup documentation (#1704) 2022-04-02 09:54:59 -07:00
Amir Raminfar
779a0f3ce9 Update modules 2022-03-29 10:35:44 -07:00
Amir Raminfar
4df32a5cef Update modules 2022-03-29 10:23:41 -07:00
Amir Raminfar
074fd8088f Updates components 2022-03-28 10:41:37 -07:00
Amir Raminfar
713ccddcf4 Updates tests 2022-03-28 10:35:13 -07:00
Coteh
7b4c942a1f Increase jump to context delay to 1s for split pane mode to ensure correct line is jumped to. Single pane mode does not have this issue so the 10ms delay can remain for that mode.
Also renamed the click handler function since it technically doesn't perform the actual scroll action anymore.
2022-03-26 03:27:19 -04:00
Coteh
11c357135b Improve jump to context speed by using watch handler to react on change to filtered messages, and also use nextTick to ensure DOM is fully rendered before it starts to jump 2022-03-26 01:36:58 -04:00
Coteh
4055aca97f Merge remote-tracking branch 'origin/master' into jump-to-context 2022-03-24 16:34:34 -04:00
kodiakhq[bot]
2fb1d19d93 Merge pull request #1698 from amir20/dependabot/go_modules/github.com/docker/docker-20.10.14incompatible
Bump github.com/docker/docker from 20.10.13+incompatible to 20.10.14+incompatible
2022-03-24 09:15:26 +00:00
dependabot[bot]
94b07b300f Bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 20.10.13+incompatible to 20.10.14+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Changelog](https://github.com/moby/moby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/docker/docker/compare/v20.10.13...v20.10.14)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-24 09:10:56 +00:00
kodiakhq[bot]
ceb0de9b7f Merge pull request #1697 from amir20/go1.18
Updates to 1.18
2022-03-23 18:46:15 +00:00
Amir Raminfar
537094b5c8 Updates to 1.18 2022-03-23 11:39:58 -07:00
Amir Raminfar
856a62ee46 Release 3.11.0 2022-03-23 11:15:09 -07:00
Amir Raminfar
3cf20d9139 Updates snapshots and modules 2022-03-23 11:04:49 -07:00
Amir Raminfar
9bcbac3799 Updates snapshots 2022-03-23 10:58:24 -07:00
Amir Raminfar
7d1e8e5e37 Updates modules 2022-03-21 11:35:38 -07:00
Amir Raminfar
117e7b3eae Updates modules 2022-03-21 10:52:27 -07:00
Amir Raminfar
2470b2b177 Merge branch 'Tuurlijk-master' 2022-03-21 10:49:39 -07:00
Amir Raminfar
111ff3a198 Skips tests for now 2022-03-21 10:49:26 -07:00
dependabot[bot]
79a8195ba5 Bump github.com/stretchr/testify from 1.7.0 to 1.7.1
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.7.0 to 1.7.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.7.0...v1.7.1)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-17 11:00:13 -07:00
dependabot[bot]
52f503ce65 Bump golang from 1.18rc1-alpine to 1.18.0-alpine
Bumps golang from 1.18rc1-alpine to 1.18.0-alpine.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-17 11:00:13 -07:00
kodiakhq[bot]
1b5d12bb2a Merge pull request #1687 from amir20/dependabot/go_modules/github.com/stretchr/testify-1.7.1
Bump github.com/stretchr/testify from 1.7.0 to 1.7.1
2022-03-16 09:21:10 +00:00
kodiakhq[bot]
69703371f1 Merge pull request #1686 from amir20/dependabot/docker/golang-1.18.0-alpine
Bump golang from 1.18rc1-alpine to 1.18.0-alpine
2022-03-16 09:20:29 +00:00
dependabot[bot]
32b5fde72e Bump github.com/stretchr/testify from 1.7.0 to 1.7.1
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.7.0 to 1.7.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.7.0...v1.7.1)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-16 09:16:34 +00:00
dependabot[bot]
552b6727da Bump golang from 1.18rc1-alpine to 1.18.0-alpine
Bumps golang from 1.18rc1-alpine to 1.18.0-alpine.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-16 09:16:20 +00:00
Michiel Roos
95222a21d8 Add auto mode for colorscheme 2022-03-15 20:04:11 +01:00
kodiakhq[bot]
f5ed2d1619 Merge pull request #1683 from amir20/dependabot/github_actions/docker/build-push-action-2.10.0
Bump docker/build-push-action from 2.9.0 to 2.10.0
2022-03-15 09:18:08 +00:00
dependabot[bot]
0f65d1f59b Bump docker/build-push-action from 2.9.0 to 2.10.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 2.9.0 to 2.10.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v2.9.0...v2.10.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-15 09:13:21 +00:00
kodiakhq[bot]
2f142c9a39 Merge pull request #1681 from amir20/dependabot/go_modules/github.com/spf13/afero-1.8.2
Bump github.com/spf13/afero from 1.8.1 to 1.8.2
2022-03-14 09:19:01 +00:00
dependabot[bot]
56a5cf7ead Bump github.com/spf13/afero from 1.8.1 to 1.8.2
Bumps [github.com/spf13/afero](https://github.com/spf13/afero) from 1.8.1 to 1.8.2.
- [Release notes](https://github.com/spf13/afero/releases)
- [Commits](https://github.com/spf13/afero/compare/v1.8.1...v1.8.2)

---
updated-dependencies:
- dependency-name: github.com/spf13/afero
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-14 09:14:22 +00:00
kodiakhq[bot]
aee8d6e5a5 Merge pull request #1676 from amir20/dependabot/go_modules/github.com/docker/docker-20.10.13incompatible
Bump github.com/docker/docker from 20.10.12+incompatible to 20.10.13+incompatible
2022-03-11 09:14:13 +00:00
dependabot[bot]
ee3f8d5046 Bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 20.10.12+incompatible to 20.10.13+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Changelog](https://github.com/moby/moby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/docker/docker/compare/v20.10.12...v20.10.13)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-11 09:09:36 +00:00
Coteh
dc2c5f35e5 Align context menu button to top of line 2022-03-06 23:19:58 -05:00
Coteh
e1635a36c8 Shrink line spacing when searching even further, now looks close to how it looks when not searching
Also fix issue where the last line's dropdown is cut off and creates overflow on the events container. Fixed this by (1) moving the last dropdown up a bit and (2) setting the event container's overflow property as `hidden`
2022-03-06 16:57:56 -05:00
Coteh
8fcc5fc9cc Shrunk spacing between lines when searching
Other Changes:
- Fix horizontal scrollbar if log line exceeds width of page
- Restored style for dropdown button on top of page
- Remove unused CSS properties
2022-03-06 14:53:25 -05:00
Coteh
fdbd8b2992 Merge remote-tracking branch 'origin/master' into jump-to-context 2022-03-06 12:10:52 -05:00
Amir Raminfar
5fe2e06733 Updates modules 2022-03-05 16:51:07 -08:00
kodiakhq[bot]
9d1923661f Merge pull request #1671 from amir20/dependabot/docker/golang-1.18rc1-alpine
Bump golang from 1.17.7-alpine to 1.18rc1-alpine
2022-03-04 09:14:45 +00:00
dependabot[bot]
7807a451a0 Bump golang from 1.17.7-alpine to 1.18rc1-alpine
Bumps golang from 1.17.7-alpine to 1.18rc1-alpine.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-04 09:10:37 +00:00
kodiakhq[bot]
30cd64c68f Merge pull request #1670 from amir20/dependabot/github_actions/actions/checkout-3
Bump actions/checkout from 2.4.0 to 3
2022-03-02 09:23:11 +00:00
kodiakhq[bot]
6cc4f44199 Merge pull request #1669 from amir20/dependabot/github_actions/docker/login-action-1.14.1
Bump docker/login-action from 1.14.0 to 1.14.1
2022-03-02 09:23:02 +00:00
dependabot[bot]
a63f9b608e Bump actions/checkout from 2.4.0 to 3
Bumps [actions/checkout](https://github.com/actions/checkout) from 2.4.0 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2.4.0...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-02 09:17:27 +00:00
dependabot[bot]
57b5578104 Bump docker/login-action from 1.14.0 to 1.14.1
Bumps [docker/login-action](https://github.com/docker/login-action) from 1.14.0 to 1.14.1.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v1.14.0...v1.14.1)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-02 09:17:24 +00:00
kodiakhq[bot]
77e6644882 Merge pull request #1668 from amir20/dependabot/github_actions/actions/setup-go-3
Bump actions/setup-go from 2.2.0 to 3
2022-03-01 09:17:27 +00:00
dependabot[bot]
5323be8db1 Bump actions/setup-go from 2.2.0 to 3
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2.2.0 to 3.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v2.2.0...v3)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-01 09:11:30 +00:00
kodiakhq[bot]
c22d58616c Merge pull request #1667 from amir20/dependabot/github_actions/docker/login-action-1.14.0
Bump docker/login-action from 1.13.0 to 1.14.0
2022-02-28 09:16:00 +00:00
dependabot[bot]
9fcd8ce72d Bump docker/login-action from 1.13.0 to 1.14.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 1.13.0 to 1.14.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v1.13.0...v1.14.0)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-28 09:10:46 +00:00
Coteh
9906604a38 Merge remote-tracking branch 'origin/master' into jump-to-context 2022-02-26 16:48:04 -05:00
kodiakhq[bot]
20742d1942 Merge pull request #1666 from amir20/dependabot/github_actions/pnpm/action-setup-2.2.1
Bump pnpm/action-setup from 2.2.0 to 2.2.1
2022-02-25 09:20:35 +00:00
dependabot[bot]
ac9bdd0fab Bump pnpm/action-setup from 2.2.0 to 2.2.1
Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 2.2.0 to 2.2.1.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v2.2.0...v2.2.1)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-25 09:14:55 +00:00
kodiakhq[bot]
17baaecc28 Merge pull request #1665 from amir20/dependabot/github_actions/actions/setup-node-3
Bump actions/setup-node from 2.5.1 to 3
2022-02-25 09:14:11 +00:00
dependabot[bot]
fa1a6d449e Bump actions/setup-node from 2.5.1 to 3
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2.5.1 to 3.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v2.5.1...v3)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-25 09:09:15 +00:00
Amir Raminfar
209b746f6a Updates modules 2022-02-23 11:27:22 -08:00
kodiakhq[bot]
9bfcd52dd5 Merge pull request #1664 from amir20/dependabot/go_modules/github.com/magiconair/properties-1.8.6
Bump github.com/magiconair/properties from 1.8.5 to 1.8.6
2022-02-23 09:14:12 +00:00
kodiakhq[bot]
5c4d0523c4 Merge pull request #1663 from amir20/dependabot/github_actions/pnpm/action-setup-2.2.0
Bump pnpm/action-setup from 2.1.0 to 2.2.0
2022-02-23 09:14:00 +00:00
dependabot[bot]
c2b3680f1c Bump github.com/magiconair/properties from 1.8.5 to 1.8.6
Bumps [github.com/magiconair/properties](https://github.com/magiconair/properties) from 1.8.5 to 1.8.6.
- [Release notes](https://github.com/magiconair/properties/releases)
- [Changelog](https://github.com/magiconair/properties/blob/main/CHANGELOG.md)
- [Commits](https://github.com/magiconair/properties/compare/v1.8.5...v1.8.6)

---
updated-dependencies:
- dependency-name: github.com/magiconair/properties
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-23 09:08:40 +00:00
dependabot[bot]
7f7e95fdad Bump pnpm/action-setup from 2.1.0 to 2.2.0
Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 2.1.0 to 2.2.0.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v2.1.0...v2.2.0)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-23 09:08:27 +00:00
kodiakhq[bot]
4191a825b1 Merge pull request #1657 from amir20/dependabot/github_actions/docker/login-action-1.13.0
Bump docker/login-action from 1.12.0 to 1.13.0
2022-02-18 09:15:37 +00:00
dependabot[bot]
968f7200d6 Bump docker/login-action from 1.12.0 to 1.13.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 1.12.0 to 1.13.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v1.12.0...v1.13.0)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-18 09:10:38 +00:00
kodiakhq[bot]
0e2744750a Merge pull request #1655 from amir20/dependabot/go_modules/github.com/alexflint/go-arg-1.4.3
Bump github.com/alexflint/go-arg from 1.4.2 to 1.4.3
2022-02-17 09:16:56 +00:00
dependabot[bot]
4e575fcfe6 Bump github.com/alexflint/go-arg from 1.4.2 to 1.4.3
Bumps [github.com/alexflint/go-arg](https://github.com/alexflint/go-arg) from 1.4.2 to 1.4.3.
- [Release notes](https://github.com/alexflint/go-arg/releases)
- [Commits](https://github.com/alexflint/go-arg/compare/v1.4.2...v1.4.3)

---
updated-dependencies:
- dependency-name: github.com/alexflint/go-arg
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-17 09:11:07 +00:00
kodiakhq[bot]
8404a70e22 Merge pull request #1651 from amir20/dependabot/docker/golang-1.17.7-alpine
Bump golang from 1.17.6-alpine to 1.17.7-alpine
2022-02-11 10:08:08 +00:00
dependabot[bot]
0954b7fbd6 Bump golang from 1.17.6-alpine to 1.17.7-alpine
Bumps golang from 1.17.6-alpine to 1.17.7-alpine.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-11 10:03:06 +00:00
kodiakhq[bot]
a5f145bb97 Merge pull request #1650 from amir20/dependabot/github_actions/actions/setup-go-2.2.0
Bump actions/setup-go from 2.1.5 to 2.2.0
2022-02-10 09:37:11 +00:00
dependabot[bot]
153b3de830 Bump actions/setup-go from 2.1.5 to 2.2.0
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2.1.5 to 2.2.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v2.1.5...v2.2.0)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-10 09:31:22 +00:00
kodiakhq[bot]
feb64444f0 Merge pull request #1648 from amir20/dependabot/github_actions/pnpm/action-setup-2.1.0
Bump pnpm/action-setup from 2.0.1 to 2.1.0
2022-02-09 09:15:54 +00:00
dependabot[bot]
6d79efe77f Bump pnpm/action-setup from 2.0.1 to 2.1.0
Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 2.0.1 to 2.1.0.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v2.0.1...v2.1.0)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-09 09:10:33 +00:00
kodiakhq[bot]
1dde163418 Merge pull request #1647 from amir20/dependabot/go_modules/github.com/spf13/afero-1.8.1
Bump github.com/spf13/afero from 1.8.0 to 1.8.1
2022-02-07 09:17:10 +00:00
dependabot[bot]
3ed1d8be4c Bump github.com/spf13/afero from 1.8.0 to 1.8.1
Bumps [github.com/spf13/afero](https://github.com/spf13/afero) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/spf13/afero/releases)
- [Commits](https://github.com/spf13/afero/compare/v1.8.0...v1.8.1)

---
updated-dependencies:
- dependency-name: github.com/spf13/afero
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-07 09:11:15 +00:00
Coteh
f71d73e90c Make jump to context work on split panes 2022-02-06 00:28:10 -05:00
Coteh
44119c82e9 Tweak appearance of jump to context menu button and ensure it doesn't appear on top of the header 2022-02-04 00:44:42 -05:00
Coteh
3dc30b656c Merge branch 'master' into jump-to-context 2022-02-04 00:32:29 -05:00
kodiakhq[bot]
03208ec636 Merge pull request #1644 from amir20/dependabot/github_actions/docker/build-push-action-2.9.0
Bump docker/build-push-action from 2.8.0 to 2.9.0
2022-02-02 09:21:31 +00:00
dependabot[bot]
b21c5cac76 Bump docker/build-push-action from 2.8.0 to 2.9.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 2.8.0 to 2.9.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v2.8.0...v2.9.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-02 09:16:14 +00:00
Amir Raminfar
f22e0eadbb Release 3.10.2 2022-01-31 15:56:32 -08:00
Amir Raminfar
3bb2f9fd7b Updates modules 2022-01-31 15:56:22 -08:00
Coteh
94de10d54c Change Jump to Context menu to be a dropdown menu
I extracted the dropdown portion of the LogActionsToolbar into a separate DropdownMenu component so that it can be reused for Jump to Context.

Other Changes:
- Remove unused style in LogActionsToolbar
2022-01-30 19:49:44 -05:00
Coteh
69e28e3723 Move jump to context button to the left, make it visible only on hover, and change icon
Other Changes:
- Update the colors of jump to context button for dark and light themes
- Add a tooltip that appears after hovering jump to context button for half a second
- Vertically align lines to accomodate the jump to context button making the line slightly bigger when visible
2022-01-24 00:50:09 -05:00
Coteh
ce7a892223 Merge remote-tracking branch 'origin/master' into jump-to-context 2022-01-23 23:30:34 -05:00
Amir Raminfar
d51a4630fd Updates gitignore 2022-01-23 07:27:26 -08:00
Amir Raminfar
3733145db5 Fixes reporting 2022-01-23 07:26:11 -08:00
Amir Raminfar
6cfb42412c Uses useScroll instead of handling the scroll event myself (#1642)
* Tries to use-scroll instead of scroll listener

* Moves observer to scrollable content only

* Fixes bugs
2022-01-20 14:23:43 -08:00
kodiakhq[bot]
bb7beef6c9 Merge pull request #1641 from amir20/dependabot/github_actions/docker/build-push-action-2.8.0
Bump docker/build-push-action from 2.7.0 to 2.8.0
2022-01-19 09:18:33 +00:00
dependabot[bot]
cdc83d183c Bump docker/build-push-action from 2.7.0 to 2.8.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 2.7.0 to 2.8.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v2.7.0...v2.8.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-19 09:13:19 +00:00
Amir Raminfar
1df564d4a3 Cleans up date components 2022-01-18 10:14:49 -08:00
Amir Raminfar
d80dc105ee Uses v-bind instead for css variables 2022-01-17 13:43:46 -08:00
Amir Raminfar
57c53e43db Updates modules 2022-01-17 10:00:24 -08:00
Coteh
25c901e013 Add jump to context WIP 2022-01-16 03:14:22 -05:00
Amir Raminfar
705f2990e9 Updates modules 2022-01-14 11:49:15 -08:00
Amir Raminfar
208dcc06cc Updates modules 2022-01-13 09:44:09 -08:00
kodiakhq[bot]
d4740bd1a8 Merge pull request #1636 from amir20/vitest
Moves to vitest
2022-01-12 22:49:26 +00:00
Amir Raminfar
c19e819ec6 removes comment import 2022-01-12 14:44:33 -08:00
Amir Raminfar
efc001ef9d Fixes test snapshots 2022-01-12 14:42:37 -08:00
Amir Raminfar
ba40708240 Fixes tests 2022-01-11 09:25:27 -08:00
Amir Raminfar
8ad6d83bd6 Updates modules 2022-01-11 09:18:10 -08:00
kodiakhq[bot]
8ef18033d3 Merge pull request #1631 from amir20/dependabot/docker/golang-1.17.6-alpine
Bump golang from 1.17.5-alpine to 1.17.6-alpine
2022-01-07 09:17:37 +00:00
dependabot[bot]
6bbb337828 Bump golang from 1.17.5-alpine to 1.17.6-alpine
Bumps golang from 1.17.5-alpine to 1.17.6-alpine.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-07 09:10:17 +00:00
kodiakhq[bot]
1111b7d10c Merge pull request #1630 from amir20/dependabot/go_modules/github.com/spf13/afero-1.8.0
Bump github.com/spf13/afero from 1.7.1 to 1.8.0
2022-01-06 09:39:30 +00:00
dependabot[bot]
40c8cafc49 Bump github.com/spf13/afero from 1.7.1 to 1.8.0
Bumps [github.com/spf13/afero](https://github.com/spf13/afero) from 1.7.1 to 1.8.0.
- [Release notes](https://github.com/spf13/afero/releases)
- [Commits](https://github.com/spf13/afero/compare/v1.7.1...v1.8.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/afero
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-06 09:34:09 +00:00
kodiakhq[bot]
e14c9e1e03 Merge pull request #1629 from amir20/dependabot/go_modules/github.com/spf13/afero-1.7.1
Bump github.com/spf13/afero from 1.7.0 to 1.7.1
2021-12-29 09:40:30 +00:00
dependabot[bot]
3176ca8e1f Bump github.com/spf13/afero from 1.7.0 to 1.7.1
Bumps [github.com/spf13/afero](https://github.com/spf13/afero) from 1.7.0 to 1.7.1.
- [Release notes](https://github.com/spf13/afero/releases)
- [Commits](https://github.com/spf13/afero/compare/v1.7.0...v1.7.1)

---
updated-dependencies:
- dependency-name: github.com/spf13/afero
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-29 09:34:56 +00:00
kodiakhq[bot]
bf86052fe3 Merge pull request #1628 from amir20/dependabot/github_actions/actions/setup-node-2.5.1
Bump actions/setup-node from 2.5.0 to 2.5.1
2021-12-29 09:14:03 +00:00
dependabot[bot]
fa4bff885e Bump actions/setup-node from 2.5.0 to 2.5.1
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2.5.0 to 2.5.1.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v2.5.0...v2.5.1)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-29 09:09:08 +00:00
Amir Raminfar
962393a9e9 Updates libs 2021-12-26 13:11:41 -08:00
kodiakhq[bot]
81958847a9 Merge pull request #1627 from amir20/dependabot/go_modules/github.com/spf13/afero-1.7.0
Bump github.com/spf13/afero from 1.6.0 to 1.7.0
2021-12-23 09:42:20 +00:00
dependabot[bot]
17bc5c5da2 Bump github.com/spf13/afero from 1.6.0 to 1.7.0
Bumps [github.com/spf13/afero](https://github.com/spf13/afero) from 1.6.0 to 1.7.0.
- [Release notes](https://github.com/spf13/afero/releases)
- [Commits](https://github.com/spf13/afero/compare/v1.6.0...v1.7.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/afero
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-23 09:36:48 +00:00
kodiakhq[bot]
b7962a0c1d Merge pull request #1626 from amir20/dependabot/github_actions/actions/setup-go-2.1.5
Bump actions/setup-go from 2.1.4 to 2.1.5
2021-12-22 09:17:08 +00:00
dependabot[bot]
e215c95600 Bump actions/setup-go from 2.1.4 to 2.1.5
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2.1.4 to 2.1.5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v2.1.4...v2.1.5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-22 09:11:15 +00:00
kodiakhq[bot]
df63a90c89 Merge pull request #1625 from amir20/dependabot/github_actions/docker/login-action-1.12.0
Bump docker/login-action from 1.10.0 to 1.12.0
2021-12-21 09:13:16 +00:00
dependabot[bot]
2eb36438e8 Bump docker/login-action from 1.10.0 to 1.12.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 1.10.0 to 1.12.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v1.10.0...v1.12.0)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-21 09:08:14 +00:00
kodiakhq[bot]
bb4f79fb08 Merge pull request #1622 from amir20/dependabot/go_modules/github.com/docker/docker-20.10.12incompatible
Bump github.com/docker/docker from 20.10.11+incompatible to 20.10.12+incompatible
2021-12-14 09:34:07 +00:00
dependabot[bot]
b5ff44b003 Bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 20.10.11+incompatible to 20.10.12+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Changelog](https://github.com/moby/moby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/docker/docker/compare/v20.10.11...v20.10.12)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-14 09:28:50 +00:00
Amir Raminfar
9741804fda Updates node modules 2021-12-13 10:38:58 -08:00
Amir Raminfar
65a13bf5fc Updates node modules 2021-12-10 14:54:35 -08:00
kodiakhq[bot]
0c5b5169c7 Merge pull request #1620 from amir20/dependabot/docker/golang-1.17.5-alpine
Bump golang from 1.17.4-alpine to 1.17.5-alpine
2021-12-10 09:13:22 +00:00
dependabot[bot]
7e8cd1a708 Bump golang from 1.17.4-alpine to 1.17.5-alpine
Bumps golang from 1.17.4-alpine to 1.17.5-alpine.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-10 09:07:41 +00:00
Amir Raminfar
84ec892777 Updates modules 2021-12-07 11:05:48 -08:00
Amir Raminfar
a0e039169c Updates modules 2021-12-06 07:22:02 -08:00
kodiakhq[bot]
070721cae1 Merge pull request #1619 from amir20/dependabot/docker/golang-1.17.4-alpine
Bump golang from 1.17.3-alpine to 1.17.4-alpine
2021-12-06 09:14:21 +00:00
dependabot[bot]
b9a619afa2 Bump golang from 1.17.3-alpine to 1.17.4-alpine
Bumps golang from 1.17.3-alpine to 1.17.4-alpine.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-06 09:08:23 +00:00
Amir Raminfar
33fb99ca37 Release 3.10.1 2021-12-01 12:15:25 -08:00
Amir Raminfar
e126d6e825 Updates modules 2021-12-01 09:45:21 -08:00
Amir Raminfar
ba1ccc92a8 Cleans up code 2021-12-01 09:44:49 -08:00
Amir Raminfar
5cf74e3f95 Add reactiviy to fix some regression issues. See #1613 2021-11-30 19:56:29 -08:00
Amir Raminfar
2246b58aa9 Adds more logging for fetching logs between dates 2021-11-30 18:48:32 -08:00
kodiakhq[bot]
f0bd0f2c9b Merge pull request #1614 from amir20/dependabot/github_actions/actions/setup-node-2.5.0
Bump actions/setup-node from 2.4.1 to 2.5.0
2021-11-30 09:14:49 +00:00
dependabot[bot]
e6781f06ae Bump actions/setup-node from 2.4.1 to 2.5.0
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2.4.1 to 2.5.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v2.4.1...v2.5.0)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-30 09:09:39 +00:00
Amir Raminfar
ef7ac5f2f6 Fixes release command 2021-11-29 10:53:37 -08:00
Amir Raminfar
5dc4d4c4d1 Release 3.10.0 2021-11-29 10:22:44 -08:00
Amir Raminfar
87d80d8284 Updates modules 2021-11-29 10:19:10 -08:00
Amir Raminfar
e216f54d6e fixes biuld 2021-11-27 15:48:59 -08:00
Amir Raminfar
cfa9c702d0 Revert "Updates node modules"
This reverts commit 05ae16df8b.
2021-11-27 15:46:31 -08:00
Amir Raminfar
15fa6ae8b0 Minor clean up 2021-11-27 14:26:30 -08:00
Amir Raminfar
05ae16df8b Updates node modules 2021-11-27 14:14:26 -08:00
Amir Raminfar
34232ef956 Fixes file in gzip when downloading 2021-11-27 13:40:49 -08:00
Amir Raminfar
da35a13d04 Updates snapshots and fixes bugs 2021-11-22 14:22:04 -08:00
Amir Raminfar
cdca0efd05 Adds more tests for routes 2021-11-21 19:56:10 -08:00
Amir Raminfar
320bbfe8b2 Adds more tests 2021-11-21 19:25:44 -08:00
Amir Raminfar
bf42fd4fea Adds more tests 2021-11-21 18:33:58 -08:00
Amir Raminfar
958a1463e6 Updats node packages 2021-11-21 09:33:19 -08:00
Amir Raminfar
4138630fc4 Updates packages 2021-11-21 09:08:41 -08:00
Amir Raminfar
91545f932c Fixes icon 2021-11-20 20:00:35 -08:00
Amir Raminfar
36cc93dacc Fixes types for stat 2021-11-20 19:58:49 -08:00
Amir Raminfar
43e777687d Adds container stats 2021-11-20 19:51:44 -08:00
Amir Raminfar
037a76f5c7 Fixes toolbar 2021-11-20 19:32:07 -08:00
Coteh
41c54a02eb Move actions toolbar to side menu 2021-11-20 15:23:09 -08:00
Amir Raminfar
7901c21843 Pinia (#1606)
* Migrates to pinia

* Does more pinia migration

* Fixes types

* Move pinia

* Adds search cleans up pinia

* Removes unused files

* Removes vuex

* More clean up

* Makes js tests pass

* Fixes bugs and removing active container
2021-11-20 15:22:13 -08:00
Amir Raminfar
257110bc64 Moves default dev port to 3100. See #1601 2021-11-19 09:55:44 -08:00
Amir Raminfar
e2072d35c8 Settings to useStorage (#1602)
* Uses storage for settings

* Cleans settings

* Fixes tests for storage

* Removes store.js

* Uses ts

* Fixes tests

* Removes autoprefixer
2021-11-18 14:06:54 -08:00
kodiakhq[bot]
4a303d3ffa Merge pull request #1600 from amir20/dependabot/go_modules/github.com/docker/docker-20.10.11incompatible
Bump github.com/docker/docker from 20.10.10+incompatible to 20.10.11+incompatible
2021-11-18 09:40:47 +00:00
dependabot[bot]
57d8a90000 Bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 20.10.10+incompatible to 20.10.11+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Changelog](https://github.com/moby/moby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/docker/docker/compare/v20.10.10...v20.10.11)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-18 09:33:45 +00:00
Amir Raminfar
0d54a265d9 Updats modules 2021-11-16 14:33:19 -08:00
Amir Raminfar
412a10256d Vue3 (#1594)
* WIP vue3

* WIP vue3

* WIP vue3

* Migrates to vitejs

* Fixes js tests and removes not needed modules

* Fixes unmount

* Updates to use css instead for space

* Fixes tests and rebases one more time

* Uses orgua

* Fixes migrations bugs with oruga and fixes scroll

* Fixes v-deep

* Fixes icons to prod

* Fixes icons to prod

* Adds favicon back

* Transitions some to composition api

* Updates another component to comp api

* Cleans defineProps

* Updates log messages

* Moves more to compose api

* Cleans up styles and rewrites event source

* Tries to fix DOMPurify

* Removes postcss

* WIP typescript

* Improves importing

* Converts all to ts

* Converts main to ts

* Makes changes for tsconfig

* Moves more to ts

* Adds typing to store

* More typing

* Updates to ts

* Updates the rest to ts

* Fixes computes

* Fixes unmount

* Adds cypress with custom base fixed

* Fixes jest tests

* Fixes golang tests

* Adds gitignore for cypress

* Removes int in favor of e2e with cypress

* Tries to fix int tests again

* Adds title

* Updates e2e tests

* Uses vue for isMobile

* Removes app spec

* Cleans up docker

* Adds drop down for settings

* Fixes bug with restart

* Fixes scroll up bug

* Adds tests for light mode
2021-11-16 10:55:44 -08:00
Amir Raminfar
215ea12e80 Fixes release 2021-11-09 14:53:57 -08:00
107 changed files with 5044 additions and 10751 deletions

View File

@@ -1,8 +0,0 @@
{
"presets": [["env", { "modules": false }]],
"env": {
"test": {
"presets": [["env", { "targets": { "node": "current" } }]]
}
}
}

View File

@@ -1,8 +1,8 @@
node_modules
.cache
.idea
.github
dist
.git
static
integration
demo.gif
e2e

View File

@@ -31,7 +31,7 @@ updates:
schedule:
interval: daily
- package-ecosystem: npm
directory: "/integration"
directory: "/e2e"
labels:
- "npm"
- "dependencies"

View File

@@ -9,11 +9,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2.4.0
uses: actions/checkout@v3
- name: Install Node
uses: actions/setup-node@v2.4.1
uses: actions/setup-node@v3
- name: Install pnpm
uses: pnpm/action-setup@v2.0.1
uses: pnpm/action-setup@v2.2.1
with:
version: 6.20.1
- name: Install dependencies
@@ -25,11 +25,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v2.1.4
uses: actions/setup-go@v3
with:
go-version: 1.17.x
go-version: 1.18.x
- name: Checkout code
uses: actions/checkout@v2.4.0
uses: actions/checkout@v3
- name: Run Go Tests with Coverage
run: make test SKIP_ASSET=1
int-test:
@@ -37,11 +37,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2.4.0
uses: actions/checkout@v3
- name: Build images
run: docker-compose -f integration/docker-compose.test.yml build
run: docker-compose -f e2e/docker-compose.yml build
- name: Run tests
run: docker-compose -f integration/docker-compose.test.yml run integration
run: 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
@@ -57,12 +57,12 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1.6.0
- name: Login to DockerHub
uses: docker/login-action@v1.10.0
uses: docker/login-action@v1.14.1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v2.7.0
uses: docker/build-push-action@v2.10.0
with:
push: true
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
@@ -77,13 +77,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2.4.0
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install Node
uses: actions/setup-node@v2.4.1
uses: actions/setup-node@v3
- name: Install pnpm
uses: pnpm/action-setup@v2.0.1
uses: pnpm/action-setup@v2.2.1
with:
version: 6.20.1
- name: Install dependencies
@@ -91,4 +91,4 @@ jobs:
- name: Release to Github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: pnpm release --github.release --no-increment --no-git --ci
run: pnpm run release -- --github.release --no-increment --no-git --ci

View File

@@ -10,6 +10,7 @@ jobs:
buildx:
name: Push branches and PRs
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == 'amir20/dozzle' }}
steps:
- name: Docker meta
id: meta
@@ -21,12 +22,12 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1.6.0
- name: Login to DockerHub
uses: docker/login-action@v1.10.0
uses: docker/login-action@v1.14.1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v2.7.0
uses: docker/build-push-action@v2.10.0
with:
push: true
platforms: linux/amd64

View File

@@ -12,11 +12,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2.4.0
uses: actions/checkout@v3
- name: Install Node
uses: actions/setup-node@v2.4.1
uses: actions/setup-node@v3
- name: Install pnpm
uses: pnpm/action-setup@v2.0.1
uses: pnpm/action-setup@v2.2.1
with:
version: 6.20.1
- name: Install dependencies
@@ -28,11 +28,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v2.1.4
uses: actions/setup-go@v3
with:
go-version: 1.17.x
go-version: 1.18.x
- name: Checkout code
uses: actions/checkout@v2.4.0
uses: actions/checkout@v3
- name: Run Go Tests with Coverage
run: make test SKIP_ASSET=1
int-test:
@@ -40,8 +40,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2.4.0
uses: actions/checkout@v3
- name: Build images
run: docker-compose -f integration/docker-compose.test.yml build
run: docker-compose -f e2e/docker-compose.yml build
- name: Run tests
run: docker-compose -f integration/docker-compose.test.yml run integration
run: docker-compose -f e2e/docker-compose.yml up --build --force-recreate --exit-code-from cypress

4
.gitignore vendored
View File

@@ -3,6 +3,6 @@ dist
node_modules
.cache
static
a_main-packr.go
dozzle
gin-bin
coverage
.pnpm-debug.log

View File

@@ -10,7 +10,7 @@ COPY pnpm-lock.yaml ./
RUN pnpm fetch --prod
# Copy files
COPY package.json .* webpack*.js ./
COPY package.json .* vite.config.ts index.html ./
# Copy assets to build
COPY assets ./assets
@@ -21,7 +21,7 @@ RUN pnpm install -r --offline --prod
# Do the build
RUN pnpm build
FROM golang:1.17.3-alpine AS builder
FROM golang:1.18.1-alpine AS builder
RUN apk add --no-cache git ca-certificates && mkdir /dozzle
@@ -32,10 +32,13 @@ COPY go.* ./
RUN go mod download
# Copy assets built with node
COPY --from=node /build/static ./static
COPY --from=node /build/dist ./dist
# Copy all other files
COPY . .
COPY analytics ./analytics
COPY docker ./docker
COPY web ./web
COPY main.go ./
# Args
ARG TAG=dev

View File

@@ -1,24 +1,24 @@
.PHONY: clean
clean:
@rm -rf static
@rm -rf dist
@go clean -i
.PHONY: static
static:
.PHONY: dist
dist:
@pnpm build
.PHONY: fake_static
fake_static:
.PHONY: fake_assets
fake_assets:
@echo 'Skipping asset build'
@mkdir -p static
@echo "assets build was skipped" > static/index.html
@mkdir -p dist
@echo "assets build was skipped" > dist/index.html
.PHONY: test
test: fake_static
test: fake_assets
go test -cover ./...
.PHONY: build
build: static
build: dist
CGO_ENABLED=0 go build -ldflags "-s -w"
.PHONY: docker
@@ -31,4 +31,4 @@ dev:
.PHONY: int
int:
docker-compose -f integration/docker-compose.test.yml up --build --force-recreate --exit-code-from integration
docker-compose -f e2e/docker-compose.yml up --build --force-recreate --exit-code-from cypress

View File

@@ -168,8 +168,8 @@ Dozzle has a [special route](https://github.com/amir20/dozzle/blob/master/assets
To Build and test locally:
1. Install NodeJs.
2. Install Go.
3. Install [reflex](https://github.com/cespare/reflex) with `get -u github.com/cespare/reflex` outside of dozzle.
4. Install node modules with `pnpm`.
1. Install [NodeJs](https://nodejs.org/en/download/) and [pnpm](https://pnpm.io/installation).
2. Install [Go](https://go.dev/doc/install).
3. Install [reflex](https://github.com/cespare/reflex) with `go get -u github.com/cespare/reflex` outside of dozzle.
4. Install node modules `pnpm install`.
5. Do `pnpm dev`

View File

@@ -1,64 +0,0 @@
import EventSource from "eventsourcemock";
import { shallowMount, RouterLinkStub, createLocalVue } from "@vue/test-utils";
import Vuex from "vuex";
import App from "./App";
jest.mock("./store/config.js", () => ({ base: "" }));
jest.mock("~icons/octicon/download-24", () => {}, { virtual: true });
jest.mock("~icons/octicon/trash-24", () => {}, { virtual: true });
jest.mock("~icons/mdi-light/chevron-double-down", () => {}, { virtual: true });
jest.mock("~icons/mdi-light/chevron-left", () => {}, { virtual: true });
jest.mock("~icons/mdi-light/chevron-right", () => {}, { virtual: true });
jest.mock("~icons/mdi-light/magnify", () => {}, { virtual: true });
jest.mock("~icons/cil/columns", () => {}, { virtual: true });
jest.mock("~icons/octicon/container-24", () => {}, { virtual: true });
jest.mock("~icons/mdi-light/cog", () => {}, { virtual: true });
const localVue = createLocalVue();
localVue.use(Vuex);
describe("<App />", () => {
const stubs = { RouterLink: RouterLinkStub, "router-view": true, "chevron-left-icon": true };
let store;
beforeEach(() => {
global.EventSource = EventSource;
const state = {
settings: { menuWidth: 15 },
containers: [{ id: "abc", name: "Test 1" }],
};
const getters = {
visibleContainers(store) {
return store.containers;
},
activeContainers() {
return [];
},
};
store = new Vuex.Store({
state,
getters,
});
});
test("has right title", async () => {
const wrapper = shallowMount(App, { stubs, store, localVue });
wrapper.vm.$store.state.containers = [
{ id: "abc", name: "Test 1" },
{ id: "xyz", name: "Test 2" },
];
await wrapper.vm.$nextTick();
expect(wrapper.vm.title).toContain("2 containers");
});
test("renders correctly", async () => {
const wrapper = shallowMount(App, { stubs, store, localVue });
await wrapper.vm.$nextTick();
expect(wrapper.element).toMatchSnapshot();
});
});

View File

@@ -3,7 +3,7 @@
<mobile-menu v-if="isMobile && !authorizationNeeded"></mobile-menu>
<splitpanes @resized="onResized($event)">
<pane min-size="10" :size="settings.menuWidth" v-if="!authorizationNeeded && !isMobile && !collapseNav">
<pane min-size="10" :size="menuWidth" v-if="!authorizationNeeded && !isMobile && !collapseNav">
<side-menu @search="showFuzzySearch"></side-menu>
</pane>
<pane min-size="10">
@@ -18,7 +18,7 @@
show-title
scrollable
closable
@close="removeActiveContainer(other)"
@close="containerStore.removeActiveContainer(other)"
></log-container>
</pane>
</template>
@@ -27,128 +27,109 @@
</splitpanes>
<button
@click="collapseNav = !collapseNav"
class="button is-small is-rounded is-settings-control"
class="button is-rounded"
:class="{ collapsed: collapseNav }"
id="hide-nav"
v-if="!isMobile && !authorizationNeeded"
>
<span class="icon ml-2" v-if="collapseNav">
<chevron-right-icon />
<mdi-light-chevron-right />
</span>
<span class="icon" v-else>
<chevron-left-icon />
<mdi-light-chevron-left />
</span>
</button>
</main>
</template>
<script>
import { mapActions, mapGetters, mapState } from "vuex";
<script lang="ts" setup>
import { Splitpanes, Pane } from "splitpanes";
import { ref, onMounted, watchEffect } from "vue";
import { storeToRefs } from "pinia";
import { useProgrammatic } from "@oruga-ui/oruga-next";
import hotkeys from "hotkeys-js";
import LogContainer from "./components/LogContainer";
import SideMenu from "./components/SideMenu";
import MobileMenu from "./components/MobileMenu";
import { setTitle } from "@/composables/title";
import { isMobile } from "@/composables/media";
import { smallerScrollbars, lightTheme, menuWidth } from "@/composables/settings";
import { useContainerStore } from "@/stores/container";
import config from "@/stores/config";
import PastTime from "./components/PastTime";
import FuzzySearchModal from "./components/FuzzySearchModal";
import FuzzySearchModal from "@/components/FuzzySearchModal.vue";
import LogContainer from "@/components/LogContainer.vue";
import SideMenu from "@/components/SideMenu.vue";
import MobileMenu from "@/components/MobileMenu.vue";
import ChevronLeftIcon from "~icons/mdi-light/chevron-left";
import ChevronRightIcon from "~icons/mdi-light/chevron-right";
const collapseNav = ref(false);
const { oruga } = useProgrammatic();
const { authorizationNeeded } = config;
export default {
name: "App",
components: {
SideMenu,
LogContainer,
MobileMenu,
Splitpanes,
PastTime,
Pane,
ChevronLeftIcon,
ChevronRightIcon,
},
data() {
return {
title: "",
collapseNav: false,
};
},
metaInfo() {
return {
title: this.title,
titleTemplate: "%s - Dozzle",
};
},
mounted() {
if (this.hasSmallerScrollbars) {
document.documentElement.classList.add("has-custom-scrollbars");
}
if (this.hasLightTheme) {
const containerStore = useContainerStore();
const { activeContainers, visibleContainers } = storeToRefs(containerStore);
onMounted(() => {
if (smallerScrollbars.value) {
document.documentElement.classList.add("has-custom-scrollbars");
}
switch (lightTheme.value) {
case "dark":
document.documentElement.setAttribute("data-theme", "dark");
break;
case "light":
document.documentElement.setAttribute("data-theme", "light");
}
this.menuWidth = this.settings.menuWidth;
hotkeys("command+k, ctrl+k", (event, handler) => {
event.preventDefault();
this.showFuzzySearch();
});
},
watch: {
hasSmallerScrollbars(newValue, oldValue) {
if (newValue) {
document.documentElement.classList.add("has-custom-scrollbars");
} else {
document.documentElement.classList.remove("has-custom-scrollbars");
}
},
hasLightTheme(newValue, oldValue) {
if (newValue) {
document.documentElement.setAttribute("data-theme", "light");
} else {
document.documentElement.removeAttribute("data-theme");
}
},
visibleContainers() {
this.title = `${this.visibleContainers.length} containers`;
},
},
computed: {
...mapState(["isMobile", "settings", "containers", "authorizationNeeded"]),
...mapGetters(["visibleContainers", "activeContainers"]),
hasSmallerScrollbars() {
return this.settings.smallerScrollbars;
},
hasLightTheme() {
return this.settings.lightTheme;
},
},
methods: {
...mapActions({
removeActiveContainer: "REMOVE_ACTIVE_CONTAINER",
updateSetting: "UPDATE_SETTING",
}),
onResized(e) {
if (e.length == 2) {
const menuWidth = e[0].size;
this.updateSetting({ menuWidth });
}
},
showFuzzySearch() {
this.$buefy.modal.open({
parent: this,
component: FuzzySearchModal,
animation: "false",
width: 600,
});
},
},
};
break;
default:
document.documentElement.removeAttribute("data-theme");
}
hotkeys("command+k, ctrl+k", (event, handler) => {
event.preventDefault();
showFuzzySearch();
});
});
watchEffect(() => {
setTitle(`${visibleContainers.value.length} containers`);
});
watchEffect(() => {
if (smallerScrollbars.value) {
document.documentElement.classList.add("has-custom-scrollbars");
} else {
document.documentElement.classList.remove("has-custom-scrollbars");
}
switch (lightTheme.value) {
case "dark":
document.documentElement.setAttribute("data-theme", "dark");
break;
case "light":
document.documentElement.setAttribute("data-theme", "light");
break;
default:
document.documentElement.removeAttribute("data-theme");
}
});
function showFuzzySearch() {
oruga.modal.open({
// parent: this,
component: FuzzySearchModal,
animation: "false",
width: 600,
active: true,
});
}
function onResized(e) {
if (e.length == 2) {
menuWidth.value = e[0].size;
}
}
</script>
<style scoped lang="scss">
::v-deep .splitpanes--vertical > .splitpanes__splitter {
:deep(.splitpanes--vertical > .splitpanes__splitter) {
min-width: 3px;
background: var(--border-color);
&:hover {

View File

@@ -1,50 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<App /> renders correctly 1`] = `
<main>
<!---->
<splitpanes-stub
dblclicksplitter="true"
pushotherpanes="true"
>
<pane-stub
maxsize="100"
minsize="10"
size="15"
>
<side-menu-stub />
</pane-stub>
<pane-stub
maxsize="100"
minsize="10"
>
<splitpanes-stub
dblclicksplitter="true"
pushotherpanes="true"
>
<pane-stub
class="has-min-height router-view"
maxsize="100"
minsize="0"
>
<router-view-stub />
</pane-stub>
</splitpanes-stub>
</pane-stub>
</splitpanes-stub>
<button
class="button is-small is-rounded is-settings-control"
id="hide-nav"
>
<span
class="icon"
>
<chevron-left-icon-stub />
</span>
</button>
</main>
`;

32
assets/components.d.ts vendored Normal file
View File

@@ -0,0 +1,32 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/vue-next/pull/3399
import '@vue/runtime-core'
declare module '@vue/runtime-core' {
export interface GlobalComponents {
CilFindInPage: typeof import('~icons/cil/find-in-page')['default']
ContainerStat: typeof import('./components/ContainerStat.vue')['default']
ContainerTitle: typeof import('./components/ContainerTitle.vue')['default']
DropdownMenu: typeof import('./components/DropdownMenu.vue')['default']
FuzzySearchModal: typeof import('./components/FuzzySearchModal.vue')['default']
InfiniteLoader: typeof import('./components/InfiniteLoader.vue')['default']
LogActionsToolbar: typeof import('./components/LogActionsToolbar.vue')['default']
LogContainer: typeof import('./components/LogContainer.vue')['default']
LogEventSource: typeof import('./components/LogEventSource.vue')['default']
LogViewer: typeof import('./components/LogViewer.vue')['default']
LogViewerWithSource: typeof import('./components/LogViewerWithSource.vue')['default']
MdiDotsVertical: typeof import('~icons/mdi/dots-vertical')['default']
MobileMenu: typeof import('./components/MobileMenu.vue')['default']
PastTime: typeof import('./components/PastTime.vue')['default']
RelativeTime: typeof import('./components/RelativeTime.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ScrollableView: typeof import('./components/ScrollableView.vue')['default']
ScrollProgress: typeof import('./components/ScrollProgress.vue')['default']
Search: typeof import('./components/Search.vue')['default']
SideMenu: typeof import('./components/SideMenu.vue')['default']
}
}
export {}

View File

@@ -4,37 +4,44 @@
{{ state }}
</div>
<div class="column is-narrow" v-if="stat.memoryUsage !== null">
<span class="has-text-weight-light">mem</span>
<span class="has-text-weight-light has-spacer">mem</span>
<span class="has-text-weight-bold">
{{ formatBytes(stat.memoryUsage) }}
</span>
</div>
<div class="column is-narrow" v-if="stat.cpu !== null">
<span class="has-text-weight-light">load</span>
<span class="has-text-weight-light has-spacer">load</span>
<span class="has-text-weight-bold"> {{ stat.cpu }}% </span>
</div>
</div>
</template>
<script>
export default {
props: {
stat: Object,
state: String,
<script lang="ts" setup>
import { ContainerStat } from "@/types/Container";
import { PropType } from "vue";
defineProps({
stat: {
type: Object as PropType<ContainerStat>,
required: true,
},
name: "ContainerStat",
methods: {
formatBytes(bytes, decimals = 2) {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
},
},
};
state: String,
});
function formatBytes(bytes: number, decimals = 2) {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
}
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.has-spacer {
&::after {
content: " ";
}
}
</style>

View File

@@ -7,13 +7,15 @@
</div>
</template>
<script>
export default {
props: {
container: Object,
<script lang="ts" setup>
import { Container } from "@/types/Container";
import { PropType } from "vue";
defineProps({
container: {
type: Object as PropType<Container>,
required: true,
},
name: "ContainerTitle",
};
});
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,44 @@
<template>
<div class="dropdown is-hoverable">
<div class="dropdown-trigger">
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu">
<span class="icon">
<mdi-dots-vertical />
</span>
</button>
</div>
<div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-content">
<slot></slot>
</div>
</div>
</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped>
.minimal .button {
background-color: rgba(0, 0, 0, 0);
border: none;
padding: 0.1em;
height: 100%;
& > .icon {
height: 100%;
& > svg {
align-self: flex-start;
height: 0.85em;
}
}
}
.is-top {
& .dropdown-menu {
top: 0;
}
&.is-last .dropdown-menu {
top: -30px;
}
}
</style>

View File

@@ -1,9 +1,9 @@
<template>
<div class="panel">
<b-autocomplete
<o-autocomplete
ref="autocomplete"
v-model="query"
placeholder="Search containers using ⌘ + k, ⌃k"
placeholder="Search containers using ⌘ + k or ctrl + k"
field="name"
open-on-focus
keep-first
@@ -11,11 +11,11 @@
:data="results"
@select="selected"
>
<template slot-scope="props">
<template #default="props">
<div class="media">
<div class="media-left">
<span class="icon is-small" :class="props.option.state">
<container-icon />
<octicon-container-24 />
</span>
</div>
<div class="media-content">
@@ -23,93 +23,84 @@
</div>
<div class="media-right">
<span class="icon is-small column-icon" @click.stop.prevent="addColumn(props.option)" title="Pin as column">
<columns-icon />
<cil-columns />
</span>
</div>
</div>
</template>
</b-autocomplete>
</o-autocomplete>
</div>
</template>
<script>
import { mapState, mapActions } from "vuex";
<script lang="ts" setup>
import fuzzysort from "fuzzysort";
import { computed, nextTick, onMounted, ref, reactive } from "vue";
import { useRouter } from "vue-router";
import { useContainerStore } from "@/stores/container";
import { storeToRefs } from "pinia";
import { Container } from "@/types/Container";
import PastTime from "./PastTime";
import ContainerIcon from "~icons/octicon/container-24";
import ColumnsIcon from "~icons/cil/columns";
const props = defineProps({
maxResults: {
default: 20,
type: Number,
},
});
export default {
props: {
maxResults: {
default: 20,
type: Number,
},
},
data() {
return {
query: "",
};
},
name: "FuzzySearchModal",
components: {
PastTime,
ContainerIcon,
ColumnsIcon,
},
mounted() {
this.$nextTick(() => this.$refs.autocomplete.focus());
},
watch: {},
methods: {
...mapActions({
appendActiveContainer: "APPEND_ACTIVE_CONTAINER",
}),
selected(item) {
this.$router.push({ name: "container", params: { id: item.id, name: item.name } });
this.$emit("close");
},
addColumn(container) {
this.appendActiveContainer(container);
this.$emit("close");
},
},
computed: {
...mapState(["containers"]),
preparedContainers() {
return this.containers.map((c) => ({
name: c.name,
id: c.id,
created: c.created,
state: c.state,
preparedName: fuzzysort.prepare(c.name),
}));
},
results() {
const options = {
limit: this.maxResults,
key: "preparedName",
};
if (this.query) {
const results = fuzzysort.go(this.query, this.preparedContainers, options);
results.forEach((result) => {
if (result.obj.state === "running") {
result.score += 1;
}
});
return results.sort((a, b) => b.score - a.score).map((i) => i.obj);
} else {
return [...this.containers].sort((a, b) => b.created - a.created);
const emit = defineEmits(["close"]);
const query = ref("");
const autocomplete = ref<HTMLElement>();
const router = useRouter();
const store = useContainerStore();
const { containers } = storeToRefs(store);
const preparedContainers = computed(() =>
containers.value.map(({ name, id, created, state }) =>
reactive({
name,
id,
created,
state,
preparedName: fuzzysort.prepare(name),
})
)
);
const results = computed(() => {
const options = {
limit: props.maxResults,
key: "preparedName",
};
if (query.value) {
const results = fuzzysort.go(query.value, preparedContainers.value, options);
results.forEach((result) => {
if (result.obj.state === "running") {
// @ts-ignore
result.score += 1;
}
},
},
};
});
return [...results].sort((a, b) => b.score - a.score).map((i) => i.obj);
} else {
return [...preparedContainers.value].sort((a, b) => b.created - a.created);
}
});
onMounted(() => nextTick(() => autocomplete.value?.focus()));
function selected(item: { id: string; name: string }) {
router.push({ name: "container", params: { id: item.id, name: item.name } });
emit("close");
}
function addColumn(container: Container) {
store.appendActiveContainer(container);
emit("close");
}
</script>
<style lang="scss" scoped>
.panel {
min-height: 400px;
width: 580px;
}
.running {
@@ -126,7 +117,7 @@ export default {
}
}
::v-deep a.dropdown-item {
:deep(a.dropdown-item) {
padding-right: 1em;
.media-right {
visibility: hidden;

View File

@@ -1,5 +1,5 @@
<template>
<div ref="observer" class="infinte-loader">
<div ref="root" class="infinte-loader">
<div class="spinner" v-show="isLoading">
<div class="bounce1"></div>
<div class="bounce2"></div>
@@ -8,40 +8,34 @@
</div>
</template>
<script>
export default {
name: "InfiniteLoader",
data() {
return {
isLoading: false,
};
},
props: {
onLoadMore: Function,
enabled: Boolean,
},
mounted() {
const intersectionObserver = new IntersectionObserver(
async (entries) => {
if (entries[0].intersectionRatio <= 0) return;
if (this.onLoadMore && this.enabled) {
const scrollingParent = this.$el.closest("[data-scrolling]") || document.documentElement;
const previousHeight = scrollingParent.scrollHeight;
this.isLoading = true;
await this.onLoadMore();
this.isLoading = false;
this.$nextTick(() => (scrollingParent.scrollTop += scrollingParent.scrollHeight - previousHeight));
}
},
{ threshholds: 1 }
);
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, nextTick } from "vue";
intersectionObserver.observe(this.$refs.observer);
const props = defineProps({
onLoadMore: Function,
enabled: Boolean,
});
this.$once("hook:beforeDestroy", () => intersectionObserver.disconnect());
},
};
const isLoading = ref(false);
const root = ref<HTMLElement>();
const observer = new IntersectionObserver(async (entries) => {
if (entries[0].intersectionRatio <= 0) return;
if (props.onLoadMore && props.enabled) {
const scrollingParent = root.value.closest("[data-scrolling]") || document.documentElement;
const previousHeight = scrollingParent.scrollHeight;
isLoading.value = true;
await props.onLoadMore();
isLoading.value = false;
await nextTick();
scrollingParent.scrollTop += scrollingParent.scrollHeight - previousHeight;
}
});
onMounted(() => observer.observe(root.value));
onUnmounted(() => observer.disconnect());
</script>
<style scoped lang="scss">
.infinte-loader {
min-height: 1px;

View File

@@ -1,62 +1,77 @@
<template>
<div class="toolbar mr-0 is-vcentered is-hidden-mobile">
<div class="is-flex">
<b-tooltip type="is-dark" label="Clear">
<a @click="onClearClicked" class="button is-small is-light is-inverted pl-1 pr-1" id="clear">
<clear-icon />
</a>
</b-tooltip>
<div class="is-flex-grow-1"></div>
<b-tooltip type="is-dark" label="Download">
<a
class="button is-small is-light is-inverted pl-1 pr-1"
id="download"
:href="`${base}/api/logs/download?id=${container.id}`"
download
>
<download-icon />
</a>
</b-tooltip>
</div>
</div>
<dropdown-menu class="is-right">
<a class="dropdown-item" @click="onClearClicked">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<octicon-trash-24 class="mr-4" />
</div>
</div>
<div class="level-right">
<div class="level-item">Clear</div>
</div>
</div>
</a>
<a class="dropdown-item" :href="`${base}/api/logs/download?id=${container.id}`">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<octicon-download-24 class="mr-4" />
</div>
</div>
<div class="level-right">
<div class="level-item">Download</div>
</div>
</div>
</a>
<hr class="dropdown-divider" />
<a class="dropdown-item" @click="showSearch = true">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<mdi-light-magnify class="mr-4" />
</div>
</div>
<div class="level-right">
<div class="level-item">Search</div>
</div>
</div>
</a>
</dropdown-menu>
</template>
<script>
import config from "../store/config";
<script lang="ts" setup>
import { onMounted, onUnmounted, PropType } from "vue";
import hotkeys from "hotkeys-js";
import DownloadIcon from "~icons/octicon/download-24";
import ClearIcon from "~icons/octicon/trash-24";
import config from "@/stores/config";
import { Container } from "@/types/Container";
import { useSearchFilter } from "@/composables/search";
export default {
props: {
onClearClicked: {
type: Function,
default: () => {},
},
container: {
type: Object,
},
const { showSearch } = useSearchFilter();
const { base } = config;
const props = defineProps({
onClearClicked: {
type: Function as PropType<(e: Event) => void>,
default: (e: Event) => {},
},
name: "LogActionsToolbar",
components: {
DownloadIcon,
ClearIcon,
},
computed: {
base() {
return config.base;
},
},
mounted() {
hotkeys("shift+command+l, shift+ctrl+l", (event, handler) => {
this.onClearClicked();
event.preventDefault();
});
container: {
type: Object as () => Container,
required: true,
},
});
const onHotkey = (event: Event) => {
props.onClearClicked(event);
event.preventDefault();
};
onMounted(() => hotkeys("shift+command+l, shift+ctrl+l", onHotkey));
onUnmounted(() => hotkeys.unbind("shift+command+l, shift+ctrl+l", onHotkey));
</script>
<style>
<style lang="scss" scoped>
#download.button,
#clear.button {
.icon {
@@ -68,27 +83,4 @@ export default {
border-color: var(--primary-color);
}
}
.toolbar {
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 200px;
background-color: var(--action-toolbar-background-color);
border-radius: 8em;
margin-top: 0.5em;
& > div {
margin: 0 2em;
padding: 0.5em 0;
}
.button {
background-color: rgba(0, 0, 0, 0) !important;
&.is-small {
font-size: 0.65rem;
}
}
}
</style>

View File

@@ -1,66 +1,64 @@
<template>
<scrollable-view :scrollable="scrollable" v-if="container">
<template v-slot:header v-if="showTitle">
<template #header v-if="showTitle">
<div class="mr-0 columns is-vcentered is-marginless is-hidden-mobile">
<div class="column is-clipped is-paddingless">
<container-title :container="container" @close="$emit('close')"></container-title>
<container-title :container="container" @close="$emit('close')" />
</div>
<div class="column is-narrow is-paddingless">
<container-stat :stat="container.stat" :state="container.state"></container-stat>
<container-stat :stat="container.stat" :state="container.state" v-if="container.stat" />
</div>
<div class="column is-narrow is-paddingless" v-if="closable">
<button class="delete is-medium" @click="$emit('close')"></button>
<div class="mr-2 column is-narrow is-paddingless">
<log-actions-toolbar :container="container" :onClearClicked="onClearClicked" />
</div>
<div class="mr-2 column is-narrow is-paddingless" v-if="closable">
<button class="delete is-medium" @click="emit('close')"></button>
</div>
</div>
<log-actions-toolbar :container="container" :onClearClicked="onClearClicked"></log-actions-toolbar>
</template>
<template v-slot="{ setLoading }">
<log-viewer-with-source ref="logViewer" :id="id" @loading-more="setLoading($event)"></log-viewer-with-source>
<template #default="{ setLoading }">
<log-viewer-with-source ref="viewer" :id="id" @loading-more="setLoading($event)" />
</template>
</scrollable-view>
</template>
<script>
import LogViewerWithSource from "./LogViewerWithSource";
import LogActionsToolbar from "./LogActionsToolbar";
import ScrollableView from "./ScrollableView";
import ContainerTitle from "./ContainerTitle";
import ContainerStat from "./ContainerStat";
import containerMixin from "./mixins/container";
<script lang="ts" setup>
import { ref, toRefs } from "vue";
import LogViewerWithSource from "./LogViewerWithSource.vue";
import { useContainerStore } from "@/stores/container";
export default {
mixins: [containerMixin],
props: {
id: {
type: String,
},
showTitle: {
type: Boolean,
default: false,
},
scrollable: {
type: Boolean,
default: false,
},
closable: {
type: Boolean,
default: false,
},
const props = defineProps({
id: {
type: String,
required: true,
},
name: "LogContainer",
components: {
LogViewerWithSource,
LogActionsToolbar,
ScrollableView,
ContainerTitle,
ContainerStat,
showTitle: {
type: Boolean,
default: false,
},
methods: {
onClearClicked() {
this.$refs.logViewer.clear();
},
scrollable: {
type: Boolean,
default: false,
},
};
closable: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["close"]);
const { id } = toRefs(props);
const store = useContainerStore();
const container = store.currentContainer(id);
const viewer = ref<InstanceType<typeof LogViewerWithSource>>();
function onClearClicked() {
viewer.value?.clear();
}
</script>
<style lang="scss" scoped>
button.delete {

View File

@@ -1,81 +1,95 @@
import debounce from "lodash.debounce";
import EventSource from "eventsourcemock";
import { sources } from "eventsourcemock";
import { shallowMount, mount, createLocalVue } from "@vue/test-utils";
import Vuex from "vuex";
import { mount } from "@vue/test-utils";
import { createTestingPinia } from "@pinia/testing";
// @ts-ignore
import EventSource, { sources } from "eventsourcemock";
import LogEventSource from "./LogEventSource.vue";
import LogViewer from "./LogViewer.vue";
import { settings } from "../composables/settings";
import { useSearchFilter } from "@/composables/search";
import { vi, describe, expect, beforeEach, test, beforeAll, afterAll } from "vitest";
import { computed, Ref } from "vue";
jest.mock("lodash.debounce", () =>
jest.fn((fn) => {
vi.mock("lodash.debounce", () => ({
__esModule: true,
default: vi.fn((fn) => {
fn.cancel = () => {};
return fn;
})
);
}),
}));
jest.mock("../store/config.js", () => ({ base: "" }));
describe("<LogEventSource />", () => {
beforeEach(() => {
global.EventSource = EventSource;
window.scrollTo = jest.fn();
const observe = jest.fn();
const disconnect = jest.fn();
global.IntersectionObserver = jest.fn(() => ({
observe,
disconnect,
}));
debounce.mockClear();
});
function createLogEventSource({ hourStyle = "auto", searchFilter = null } = {}) {
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.component("log-viewer", LogViewer);
const state = { searchFilter, settings: { size: "medium", showTimestamp: true, hourStyle } };
const getters = {
allContainersById() {
return {
abc: { state: "running" },
};
vi.mock("@/stores/container", () => ({
__esModule: true,
useContainerStore() {
return {
currentContainer(id: Ref<string>) {
return computed(() => ({ id: id.value }));
},
};
},
}));
const store = new Vuex.Store({
state,
getters,
});
vi.mock("@/stores/config", () => ({
__esModule: true,
default: { base: "" },
}));
/**
* @vitest-environment jsdom
*/
describe("<LogEventSource />", () => {
const search = useSearchFilter();
beforeEach(() => {
global.EventSource = EventSource;
window.scrollTo = vi.fn();
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
disconnect: vi.fn(),
}));
});
function createLogEventSource(
{
searchFilter = undefined,
hourStyle = "auto",
}: { searchFilter?: string | undefined; hourStyle?: "auto" | "24" | "12" } = {
hourStyle: "auto",
}
) {
settings.value.hourStyle = hourStyle;
search.searchFilter.value = searchFilter;
return mount(LogEventSource, {
localVue,
store,
scopedSlots: {
global: {
plugins: [createTestingPinia({ createSpy: vi.fn })],
components: {
LogViewer,
},
},
slots: {
default: `
<log-viewer :messages="props.messages"></log-viewer>
<template #scoped="params"><log-viewer :messages="params.messages"></log-viewer></template>
`,
},
propsData: { id: "abc" },
props: { id: "abc" },
});
}
test("renders correctly", async () => {
const wrapper = createLogEventSource();
expect(wrapper.element).toMatchSnapshot();
expect(wrapper.html()).toMatchSnapshot();
});
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);
wrapper.destroy();
wrapper.unmount();
});
test("should close EventSource", async () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
wrapper.destroy();
wrapper.unmount();
expect(sources["/api/logs/stream?id=abc&lastEventId="].readyState).toBe(2);
});
@@ -90,12 +104,7 @@ describe("<LogEventSource />", () => {
const { key, ...messageWithoutKey } = message;
expect(key).toBe("2019-06-12T10:55:42.459034602Z");
expect(messageWithoutKey).toMatchInlineSnapshot(`
Object {
"date": 2019-06-12T10:55:42.459Z,
"message": "\\"This is a message.\\"",
}
`);
expect(messageWithoutKey).toMatchSnapshot();
});
test("should parse messages with loki's timestamp format", async () => {
@@ -107,12 +116,7 @@ describe("<LogEventSource />", () => {
const { key, ...messageWithoutKey } = message;
expect(key).toBe("2020-04-27T12:35:43.272974324+02:00");
expect(messageWithoutKey).toMatchInlineSnapshot(`
Object {
"date": 2020-04-27T10:35:43.272Z,
"message": "xxxxx",
}
`);
expect(messageWithoutKey).toMatchSnapshot();
});
test("should pass messages to slot", async () => {
@@ -121,25 +125,22 @@ describe("<LogEventSource />", () => {
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
data: `2019-06-12T10:55:42.459034602Z "This is a message."`,
});
const [message, _] = wrapper.findComponent(LogViewer).vm.messages;
const [message, _] = wrapper.getComponent(LogViewer).vm.messages;
const { key, ...messageWithoutKey } = message;
expect(key).toBe("2019-06-12T10:55:42.459034602Z");
expect(messageWithoutKey).toMatchInlineSnapshot(`
Object {
"date": 2019-06-12T10:55:42.459Z,
"message": "\\"This is a message.\\"",
}
`);
expect(messageWithoutKey).toMatchSnapshot();
});
describe("render html correctly", () => {
const RealDate = Date;
beforeAll(() => {
// @ts-ignore
global.Date = class extends RealDate {
constructor(arg) {
constructor(arg: any | number) {
super(arg);
if (arg) {
return new RealDate(arg);
} else {
@@ -158,11 +159,7 @@ describe("<LogEventSource />", () => {
});
await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
<ul class="events medium">
<li><span class="date"><time datetime="2019-06-12T10:55:42.459Z">today at 10:55:42 AM</time></span> <span class="text">"This is a message."</span></li>
</ul>
`);
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
});
test("should render messages with color", async () => {
@@ -173,11 +170,7 @@ describe("<LogEventSource />", () => {
});
await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
<ul class="events medium">
<li><span class="date"><time datetime="2019-06-12T10:55:42.459Z">today at 10:55:42 AM</time></span> <span class="text"><span style="color:#000">black<span style="color:#AAA">white</span></span></span></li>
</ul>
`);
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
});
test("should render messages with html entities", async () => {
@@ -188,11 +181,7 @@ describe("<LogEventSource />", () => {
});
await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
<ul class="events medium">
<li><span class="date"><time datetime="2019-06-12T10:55:42.459Z">today at 10:55:42 AM</time></span> <span class="text">&lt;test&gt;foo bar&lt;/test&gt;</span></li>
</ul>
`);
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
});
test("should render dates with 12 hour style", async () => {
@@ -203,11 +192,7 @@ describe("<LogEventSource />", () => {
});
await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
<ul class="events medium">
<li><span class="date"><time datetime="2019-06-12T23:55:42.459Z">today at 11:55:42 PM</time></span> <span class="text">&lt;test&gt;foo bar&lt;/test&gt;</span></li>
</ul>
`);
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
});
test("should render dates with 24 hour style", async () => {
@@ -218,11 +203,7 @@ describe("<LogEventSource />", () => {
});
await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
<ul class="events medium">
<li><span class="date"><time datetime="2019-06-12T23:55:42.459Z">today at 23:55:42</time></span> <span class="text">&lt;test&gt;foo bar&lt;/test&gt;</span></li>
</ul>
`);
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
});
test("should render messages with filter", async () => {
@@ -236,11 +217,7 @@ describe("<LogEventSource />", () => {
});
await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
<ul class="events medium">
<li><span class="date"><time datetime="2019-06-12T10:55:42.459Z">today at 10:55:42 AM</time></span> <span class="text">This is a <mark>test</mark> &lt;hi&gt;&lt;/hi&gt;</span></li>
</ul>
`);
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
});
});
});

View File

@@ -1,125 +1,133 @@
<template>
<div>
<infinite-loader :onLoadMore="loadOlderLogs" :enabled="messages.length > 100"></infinite-loader>
<slot :messages="messages"></slot>
</div>
<infinite-loader :onLoadMore="loadOlderLogs" :enabled="messages.length > 100"></infinite-loader>
<slot :messages="messages"></slot>
</template>
<script>
<script lang="ts" setup>
import { toRefs, ref, watch, onUnmounted } from "vue";
import debounce from "lodash.debounce";
import InfiniteLoader from "./InfiniteLoader";
import config from "../store/config";
import containerMixin from "./mixins/container";
export default {
props: ["id"],
mixins: [containerMixin],
name: "LogEventSource",
components: {
InfiniteLoader,
},
data() {
return {
messages: [],
buffer: [],
es: null,
lastEventId: null,
};
},
created() {
this.flushBuffer = debounce(this.flushNow, 250, { maxWait: 1000 });
this.loadLogs();
},
beforeDestroy() {
this.es.close();
},
methods: {
loadLogs() {
this.reset();
this.connect();
},
onContainerStopped() {
this.es.close();
this.buffer.push({ event: "container-stopped", message: "Container stopped", date: new Date(), key: new Date() });
this.flushBuffer();
this.flushBuffer.flush();
},
onMessage(e) {
this.lastEventId = e.lastEventId;
this.buffer.push(this.parseMessage(e.data));
this.flushBuffer();
},
onContainerStateChange(newValue, oldValue) {
if (newValue == "running" && newValue != oldValue) {
this.buffer.push({
event: "container-started",
message: "Container started",
date: new Date(),
key: new Date(),
});
this.connect();
}
},
connect() {
this.es = new EventSource(`${config.base}/api/logs/stream?id=${this.id}&lastEventId=${this.lastEventId ?? ""}`);
this.es.addEventListener("container-stopped", (e) => this.onContainerStopped());
this.es.addEventListener("error", (e) => console.error("EventSource failed: " + JSON.stringify(e)));
this.es.onmessage = (e) => this.onMessage(e);
},
flushNow() {
this.messages.push(...this.buffer);
this.buffer = [];
},
clear() {
this.messages = [];
},
reset() {
if (this.es) {
this.es.close();
}
this.flushBuffer.cancel();
this.es = null;
this.messages = [];
this.buffer = [];
this.lastEventId = null;
},
async loadOlderLogs() {
if (this.messages.length < 300) return;
import { LogEntry } from "@/types/LogEntry";
import InfiniteLoader from "./InfiniteLoader.vue";
import config from "@/stores/config";
import { useContainerStore } from "@/stores/container";
this.$emit("loading-more", true);
const to = this.messages[0].date;
const last = this.messages[299].date;
const delta = to - last;
const from = new Date(to.getTime() + delta);
const logs = await (
await fetch(`${config.base}/api/logs?id=${this.id}&from=${from.toISOString()}&to=${to.toISOString()}`)
).text();
if (logs) {
const newMessages = logs
.trim()
.split("\n")
.map((line) => this.parseMessage(line));
this.messages.unshift(...newMessages);
}
this.$emit("loading-more", false);
},
parseMessage(data) {
let i = data.indexOf(" ");
if (i == -1) {
i = data.length;
}
const key = data.substring(0, i);
const date = new Date(key);
const message = data.substring(i + 1);
return { key, date, message };
},
const props = defineProps({
id: {
type: String,
required: true,
},
watch: {
id(newValue, oldValue) {
if (oldValue !== newValue) {
this.loadLogs();
}
},
},
};
});
const { id } = toRefs(props);
const emit = defineEmits(["loading-more"]);
const store = useContainerStore();
const container = store.currentContainer(id);
const messages = ref<LogEntry[]>([]);
const buffer = ref<LogEntry[]>([]);
function flushNow() {
messages.value.push(...buffer.value);
buffer.value = [];
}
const flushBuffer = debounce(flushNow, 250, { maxWait: 1000 });
let es: EventSource | null = null;
let lastEventId = "";
function connect({ clear } = { clear: true }) {
es?.close();
if (clear) {
flushBuffer.cancel();
messages.value = [];
buffer.value = [];
lastEventId = "";
}
es = new EventSource(`${config.base}/api/logs/stream?id=${props.id}&lastEventId=${lastEventId}`);
es.addEventListener("container-stopped", () => {
es?.close();
es = null;
buffer.value.push({
event: "container-stopped",
message: "Container stopped",
date: new Date(),
key: new Date().toString(),
});
flushBuffer();
flushBuffer.flush();
});
es.addEventListener("error", (e) => console.error("EventSource failed: " + JSON.stringify(e)));
es.onmessage = (e) => {
lastEventId = e.lastEventId;
if (e.data) {
buffer.value.push(parseMessage(e.data));
flushBuffer();
}
};
}
async function loadOlderLogs() {
if (messages.value.length < 300) return;
emit("loading-more", true);
const to = messages.value[0].date;
const last = messages.value[299].date;
const delta = to.getTime() - last.getTime();
const from = new Date(to.getTime() + delta);
const logs = await (
await fetch(`${config.base}/api/logs?id=${props.id}&from=${from.toISOString()}&to=${to.toISOString()}`)
).text();
if (logs) {
const newMessages = logs
.trim()
.split("\n")
.map((line) => parseMessage(line));
messages.value.unshift(...newMessages);
}
emit("loading-more", false);
}
function parseMessage(data: String): LogEntry {
let i = data.indexOf(" ");
if (i == -1) {
i = data.length;
}
const key = data.substring(0, i);
const date = new Date(key);
const message = data.substring(i + 1);
return { key, date, message };
}
watch(
() => container.value.state,
(newValue, oldValue) => {
console.log("LogEventSource: container changed", newValue, oldValue);
if (newValue == "running" && newValue != oldValue) {
buffer.value.push({
event: "container-started",
message: "Container started",
date: new Date(),
key: new Date().toString(),
});
connect({ clear: false });
}
}
);
onUnmounted(() => {
if (es) {
es.close();
}
});
connect();
watch(id, () => connect());
defineExpose({
clear: () => (messages.value = []),
});
</script>

View File

@@ -1,72 +1,101 @@
<template>
<ul class="events" :class="settings.size">
<li v-for="item in filtered" :key="item.key" :data-event="item.event">
<span class="date" v-if="settings.showTimestamp"><relative-time :date="item.date"></relative-time></span>
<span class="text" v-html="colorize(item.message)"></span>
<ul class="events" :class="size" ref="events">
<li
v-for="(item, index) in filtered"
:key="item.key"
:data-event="item.event"
:data-key="item.key"
:class="item.selected ? 'selected' : ''"
>
<div class="line-options" v-if="isSearching()">
<dropdown-menu :class="{ 'is-last': index === filtered.length - 1 }" class="is-top minimal">
<a class="dropdown-item" @click="handleJumpLineSelected">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<cil-find-in-page class="mr-4" />
</div>
</div>
<div class="level-right">
<div class="level-item">Jump to Context</div>
</div>
</div>
</a>
</dropdown-menu>
</div>
<div class="line">
<span class="date" v-if="showTimestamp"> <relative-time :date="item.date"></relative-time></span>
<span class="text" v-html="colorize(item.message)"></span>
</div>
</li>
</ul>
</template>
<script>
import { mapState } from "vuex";
<script lang="ts" setup>
import { PropType, ref, toRefs, watch, nextTick } from "vue";
import { size, showTimestamp } from "@/composables/settings";
import RelativeTime from "./RelativeTime.vue";
import AnsiConvertor from "ansi-to-html";
import DOMPurify from "dompurify";
import RelativeTime from "./RelativeTime";
import { LogEntry } from "@/types/LogEntry";
import { useSearchFilter } from "@/composables/search";
import { useContainerStore } from "@/stores/container";
import { storeToRefs } from "pinia";
const props = defineProps({
messages: {
type: Array as PropType<LogEntry[]>,
required: true,
},
});
const ansiConvertor = new AnsiConvertor({ escapeXML: true });
if (window.trustedTypes && trustedTypes.createPolicy) {
trustedTypes.createPolicy("default", {
createHTML: (string, sink) => DOMPurify.sanitize(string, { RETURN_TRUSTED_TYPE: true }),
});
}
export default {
props: ["messages"],
name: "LogViewer",
components: { RelativeTime },
data() {
return {
showSearch: false,
};
},
methods: {
colorize: function (value) {
return ansiConvertor.toHtml(value).replace("&lt;mark&gt;", "<mark>").replace("&lt;/mark&gt;", "</mark>");
},
},
computed: {
...mapState(["searchFilter", "settings"]),
filtered() {
const { searchFilter, messages } = this;
if (searchFilter) {
const isSmartCase = searchFilter === searchFilter.toLowerCase();
try {
const regex = isSmartCase ? new RegExp(searchFilter, "i") : new RegExp(searchFilter);
return messages
.filter((d) => d.message.match(regex))
.map((d) => ({
...d,
message: d.message.replace(regex, "<mark>$&</mark>"),
}));
} catch (e) {
if (e instanceof SyntaxError) {
console.info(`Ignoring SytaxError from search.`, e);
return messages;
}
throw e;
}
}
return messages;
},
},
const colorize = (value: string) =>
ansiConvertor.toHtml(value).replace("&lt;mark&gt;", "<mark>").replace("&lt;/mark&gt;", "</mark>");
const { messages } = toRefs(props);
const { filteredMessages, searchFilter, resetSearch, isSearching } = useSearchFilter();
const store = useContainerStore();
const { activeContainers } = storeToRefs(store);
const filtered = filteredMessages(messages);
const events = ref(null);
let selectedLine;
const handleJumpLineSelected = async (e) => {
const line = e.target.closest("li");
if (line.tagName !== "LI") {
return;
}
selectedLine = line;
resetSearch();
};
watch(filtered, async (newVal, oldVal) => {
if (selectedLine == null) {
return;
}
await nextTick();
for (const item of messages.value) {
item.selected = false;
if (item.key === selectedLine.dataset.key) {
item.selected = true;
}
}
// when in split pane mode - scroll the pane element, when not in split pane mode - scroll the window element
const inSplitPane = activeContainers.value.length > 0;
const elemToScroll = inSplitPane ? events.value.closest("main") : window;
elemToScroll.scrollTo(0, 0);
// wait 1s before jumping when in split pane mode to ensure the correct line is scrolled to, single pane mode does not have this issue thus only 10ms is needed
await new Promise((resolve) => setTimeout(resolve, inSplitPane ? 1000 : 10));
elemToScroll.scrollTo(0, selectedLine.offsetTop - 200);
selectedLine = null;
});
</script>
<style scoped lang="scss">
.events {
padding: 1em;
font-family: SFMono-Regular, Consolas, Liberation Mono, monaco, Menlo, monospace;
overflow: hidden;
& > li {
display: flex;
word-wrap: break-word;
line-height: 130%;
&:last-child {
@@ -79,6 +108,22 @@ export default {
&[data-event="container-started"] {
color: hsl(141, 53%, 53%);
}
&.selected {
background-color: var(--menu-item-active-background-color);
color: black;
}
&.selected > .date {
background-color: white;
}
& > .line {
margin: auto 0;
width: 100%;
}
& > .line-options {
display: flex;
flex-direction: row-reverse;
margin-right: 1em;
}
}
&.small {
@@ -94,23 +139,47 @@ export default {
}
}
.date {
background-color: #262626;
color: #258ccd;
@media (prefers-color-scheme: dark) {
.date {
background-color: #262626;
color: #258ccd;
}
}
[data-theme="light"] & {
[data-theme="dark"] {
.date {
background-color: #262626;
color: #258ccd;
}
}
@media (prefers-color-scheme: light) {
.date {
background-color: #f0f0f0;
color: #009900;
padding-left: 5px;
padding-right: 5px;
}
}
[data-theme="light"] {
.date {
background-color: #f0f0f0;
color: #009900;
}
}
.date {
padding-left: 5px;
padding-right: 5px;
}
.text {
white-space: pre-wrap;
&::before {
content: " ";
}
}
::v-deep mark {
:deep(mark) {
border-radius: 2px;
background-color: var(--secondary-color);
animation: pops 200ms ease-out;

View File

@@ -1,24 +1,26 @@
<template>
<log-event-source ref="logEventSource" :id="id" v-slot="eventSource" @loading-more="$emit('loading-more', $event)">
<log-viewer :messages="eventSource.messages"></log-viewer>
<log-event-source ref="source" :id="id" #default="{ messages }" @loading-more="emit('loading-more', $event)">
<log-viewer :messages="messages"></log-viewer>
</log-event-source>
</template>
<script>
import LogEventSource from "./LogEventSource";
import LogViewer from "./LogViewer";
<script lang="ts" setup>
import LogViewer from "./LogViewer.vue";
import { ref } from "vue";
defineProps({
id: {
type: String,
required: true,
},
});
export default {
props: ["id"],
name: "LogViewerWithSource",
components: {
LogEventSource,
LogViewer,
},
methods: {
clear() {
this.$refs.logEventSource.clear();
},
},
};
const emit = defineEmits(["loading-more"]);
const source = ref<InstanceType<typeof LogViewer>>();
function clear() {
source.value?.clear();
}
defineExpose({
clear,
});
</script>

View File

@@ -9,7 +9,7 @@
</router-link>
</div>
<div class="column ml-4 is-family-monospace is-ellipsis" v-if="$route.name == 'container'">
{{ allContainersById[$route.params.id].name }}
{{ allContainersById[route.params.id].name }}
</div>
<div class="column is-narrow push-right">
@@ -41,32 +41,26 @@
</aside>
</template>
<script>
import { mapGetters } from "vuex";
<script lang="ts" setup>
import { ref, watch } from "vue";
import { useContainerStore } from "@/stores/container";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
export default {
props: [],
name: "MobileMenu",
data() {
return {
showNav: false,
};
},
computed: {
...mapGetters(["visibleContainers", "allContainersById"]),
},
watch: {
$route(to, from) {
this.showNav = false;
},
},
};
const store = useContainerStore();
const route = useRoute();
const { visibleContainers, allContainersById } = storeToRefs(store);
const showNav = ref(false);
watch(route, () => {
showNav.value = false;
});
</script>
<style scoped lang="scss">
aside {
padding: 1em;
position: fixed;
top: 0;
left: 0;
right: 0;
background: var(--scheme-main-ter);

View File

@@ -2,38 +2,23 @@
<time :datetime="date.toISOString()">{{ text }}</time>
</template>
<script>
<script lang="ts" setup>
import { useIntervalFn } from "@vueuse/core";
import formatDistance from "date-fns/formatDistance";
import { PropType, ref } from "vue";
export default {
props: {
date: {
required: true,
type: Date,
},
const props = defineProps({
date: {
required: true,
type: Object as PropType<Date>,
},
data() {
return {
text: "",
interval: null,
};
},
name: "PastTime",
mounted() {
this.updateFromNow();
this.interval = setInterval(() => this.updateFromNow(), 30000);
},
destroyed() {
clearInterval(this.interval);
},
methods: {
updateFromNow() {
this.text = formatDistance(this.date, new Date(), {
addSuffix: true,
});
},
},
};
});
const text = ref<string>();
function updateFromNow() {
text.value = formatDistance(props.date, new Date(), {
addSuffix: true,
});
}
useIntervalFn(updateFromNow, 30_000, { immediateCallback: true });
</script>
<style scoped lang="scss"></style>

View File

@@ -1,50 +1,44 @@
<template>
<time :datetime="date.toISOString()">{{ date | relativeTime(locale) }}</time>
<time :datetime="date.toISOString()">{{ relativeTime(date, locale) }}</time>
</template>
<script>
import { mapState } from "vuex";
import { formatRelative } from "date-fns";
import enGB from "date-fns/locale/en-GB";
import enUS from "date-fns/locale/en-US";
<script lang="ts">
const use24Hr =
new Intl.DateTimeFormat(undefined, {
hour: "numeric",
})
.formatToParts(new Date(2020, 0, 1, 13))
.find((part) => part.type === "hour").value.length === 2;
.find((part) => part.type === "hour")?.value.length === 2;
const auto = use24Hr ? enGB : enUS;
const styles = { auto, 12: enUS, 24: enGB };
export default {
props: {
date: {
required: true,
type: Date,
},
},
name: "RelativeTime",
components: {},
computed: {
...mapState(["settings"]),
locale() {
const locale = styles[this.settings.hourStyle];
const oldFormatter = locale.formatRelative;
return {
...locale,
formatRelative(token) {
return oldFormatter(token) + "p";
},
};
},
},
filters: {
relativeTime(date, locale) {
return formatRelative(date, new Date(), { locale });
},
},
};
</script>
<script lang="ts" setup>
import { formatRelative } from "date-fns";
import { hourStyle } from "@/composables/settings";
import enGB from "date-fns/locale/en-GB";
import enUS from "date-fns/locale/en-US";
import { computed, PropType } from "vue";
defineProps({
date: {
required: true,
type: Object as PropType<Date>,
},
});
const locale = computed(() => {
const locale = styles[hourStyle.value];
const oldFormatter = locale.formatRelative as (d: Date | number) => string;
return {
...locale,
formatRelative(date: Date | number) {
return oldFormatter(date) + "p";
},
};
});
function relativeTime(date: Date, locale: Locale) {
return formatRelative(date, new Date(), { locale });
}
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div class="scroll-progress">
<div class="scroll-progress" ref="root">
<svg width="100" height="100" viewBox="0 0 100 100" :class="{ indeterminate }">
<circle r="44" cx="50" cy="50" :style="{ '--progress': scrollProgress }" />
<circle r="44" cx="50" cy="50" />
</svg>
<div class="is-overlay columns is-vcentered is-centered has-text-weight-light">
<template v-if="indeterminate">
@@ -17,74 +17,60 @@
</div>
</template>
<script>
import { mapGetters } from "vuex";
import throttle from "lodash.throttle";
<script lang="ts" setup>
import { useContainerStore } from "@/stores/container";
import { useScroll } from "@vueuse/core";
import { storeToRefs } from "pinia";
import { onMounted, ref, watch, watchPostEffect } from "vue";
export default {
name: "ScrollProgress",
props: {
indeterminate: {
default: false,
type: Boolean,
},
autoHide: {
default: true,
type: Boolean,
},
const props = defineProps({
indeterminate: {
default: false,
type: Boolean,
},
data() {
return {
scrollProgress: 0,
animation: { cancel: () => {} },
parentElement: document,
};
autoHide: {
default: true,
type: Boolean,
},
created() {
this.onScrollThrottled = throttle(this.onScroll, 150);
},
mounted() {
this.attachEvents();
this.$once("hook:beforeDestroy", this.detachEvents);
},
watch: {
activeContainers() {
this.detachEvents();
this.attachEvents();
});
const scrollProgress = ref(0);
const animation = ref({ cancel: () => {} });
const root = ref<HTMLElement>();
const store = useContainerStore();
const { activeContainers } = storeToRefs(store);
const scrollElement = ref<HTMLElement | Document>((root.value?.closest("[data-scrolling]") as HTMLElement) ?? document);
const { y: scrollY } = useScroll(scrollElement, { throttle: 100 });
onMounted(() => {
watch(
activeContainers,
() => {
scrollElement.value = (root.value?.closest("[data-scrolling]") as HTMLElement) ?? document;
},
indeterminate() {
this.$nextTick(() => this.onScroll());
},
},
computed: {
...mapGetters(["activeContainers"]),
},
methods: {
attachEvents() {
this.parentElement = this.$el.closest("[data-scrolling]") || document;
this.parentElement.addEventListener("scroll", this.onScrollThrottled);
},
detachEvents() {
this.parentElement.removeEventListener("scroll", this.onScrollThrottled);
},
onScroll() {
const p = this.parentElement == document ? document.documentElement : this.parentElement;
this.scrollProgress = p.scrollTop / (p.scrollHeight - p.clientHeight);
this.animation.cancel();
if (this.autoHide) {
this.animation = this.$el.animate(
{ opacity: [1, 0] },
{
duration: 500,
delay: 2000,
fill: "both",
easing: "ease-out",
}
);
{ immediate: true, flush: "post" }
);
});
watchPostEffect(() => {
const parent =
scrollElement.value === document
? (scrollElement.value as Document).documentElement
: (scrollElement.value as HTMLElement);
scrollProgress.value = scrollY.value / (parent.scrollHeight - parent.clientHeight);
animation.value.cancel();
if (props.autoHide && root.value) {
animation.value = root.value.animate(
{ opacity: [1, 0] },
{
duration: 500,
delay: 2000,
fill: "both",
easing: "ease-out",
}
},
},
};
);
}
});
</script>
<style scoped lang="scss">
.scroll-progress {
@@ -109,7 +95,7 @@ export default {
transform: rotate(-90deg);
transform-origin: 50% 50%;
stroke: var(--primary-color);
stroke-dashoffset: calc(276.32px - var(--progress) * 276.32px);
stroke-dashoffset: calc(276.32px - v-bind(scrollProgress) * 276.32px);
stroke-dasharray: 276.32px 276.32px;
stroke-linecap: round;
stroke-width: 3;

View File

@@ -3,33 +3,28 @@
<header v-if="$slots.header">
<slot name="header"></slot>
</header>
<main ref="content" :data-scrolling="scrollable">
<main :data-scrolling="scrollable ? true : undefined">
<div class="is-scrollbar-progress is-hidden-mobile">
<scroll-progress v-show="paused" :indeterminate="loading" :auto-hide="!loading"></scroll-progress>
</div>
<slot :setLoading="setLoading"></slot>
<div ref="scrollableContent">
<slot :setLoading="setLoading"></slot>
</div>
<div ref="scrollObserver" class="is-scroll-observer"></div>
</main>
<div class="is-scrollbar-notification">
<transition name="fade">
<button
class="button pl-1 pr-1"
:class="hasMore ? 'has-more' : ''"
@click="scrollToBottom('instant')"
v-show="paused"
>
<chevron-double-down-icon />
<button class="button" :class="hasMore ? 'has-more' : ''" @click="scrollToBottom('instant')" v-show="paused">
<mdi-light-chevron-double-down />
</button>
</transition>
</div>
</section>
</template>
<script>
import ScrollProgress from "./ScrollProgress";
import ChevronDoubleDownIcon from "~icons/mdi-light/chevron-double-down";
<script lang="ts">
export default {
props: {
scrollable: {
@@ -37,21 +32,20 @@ export default {
default: true,
},
},
components: {
ScrollProgress,
ChevronDoubleDownIcon,
},
name: "ScrollableView",
data() {
return {
paused: false,
hasMore: false,
loading: false,
mutationObserver: null,
intersectionObserver: null,
};
},
mounted() {
const { content } = this.$refs;
const mutationObserver = new MutationObserver((e) => {
const { scrollableContent } = this.$refs;
this.mutationObserver = new MutationObserver((e) => {
if (!this.paused) {
this.scrollToBottom("instant");
} else {
@@ -63,17 +57,18 @@ export default {
}
}
});
mutationObserver.observe(content, { childList: true, subtree: true });
this.$once("hook:beforeDestroy", () => mutationObserver.disconnect());
this.mutationObserver.observe(scrollableContent, { childList: true, subtree: true });
const intersectionObserver = new IntersectionObserver(
this.intersectionObserver = new IntersectionObserver(
(entries) => (this.paused = entries[0].intersectionRatio == 0),
{ threshholds: [0, 1], rootMargin: "80px 0px" }
);
intersectionObserver.observe(this.$refs.scrollObserver);
this.$once("hook:beforeDestroy", () => intersectionObserver.disconnect());
this.intersectionObserver.observe(this.$refs.scrollObserver);
},
beforeUnmount() {
this.mutationObserver.disconnect();
this.intersectionObserver.disconnect();
},
methods: {
scrollToBottom(behavior = "instant") {
this.$refs.scrollObserver.scrollIntoView({ behavior });
@@ -95,6 +90,7 @@ section {
top: 0;
background: var(--body-background-color);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
z-index: 1;
}
&.is-full-height-scrollable {

View File

@@ -1,17 +1,17 @@
<template>
<div class="search columns is-gapless is-vcentered" v-show="showSearch" v-if="settings.search">
<div class="search columns is-gapless is-vcentered" v-show="showSearch" v-if="search">
<div class="column">
<p class="control has-icons-left">
<input
class="input"
type="text"
placeholder="Find / RegEx"
ref="filter"
v-model="filter"
ref="input"
v-model="searchFilter"
@keyup.esc="resetSearch()"
/>
<span class="icon is-left">
<search-icon />
<mdi-light-magnify />
</span>
</p>
</div>
@@ -21,57 +21,31 @@
</div>
</template>
<script>
import { mapActions, mapState } from "vuex";
<script lang="ts" setup>
import hotkeys from "hotkeys-js";
import SearchIcon from "~icons/mdi-light/magnify";
export default {
props: [],
name: "Search",
components: {
SearchIcon,
},
data() {
return {
showSearch: false,
};
},
mounted() {
hotkeys("command+f, ctrl+f", (event, handler) => {
this.showSearch = true;
this.$nextTick(() => this.$refs.filter.focus() || this.$refs.filter.select());
event.preventDefault();
});
hotkeys("esc", (event, handler) => {
this.resetSearch();
});
},
beforeDestroy() {
this.updateSearchFilter("");
hotkeys.unbind("command+f, ctrl+f, esc");
},
methods: {
...mapActions({
updateSearchFilter: "SET_SEARCH",
}),
resetSearch() {
this.showSearch = false;
this.filter = "";
},
},
computed: {
...mapState(["searchFilter", "settings"]),
filter: {
get() {
return this.searchFilter;
},
set(value) {
this.updateSearchFilter(value);
},
},
},
};
import { search } from "@/composables/settings";
import { useSearchFilter } from "@/composables/search";
import { ref, nextTick, onMounted, onUnmounted } from "vue";
const input = ref<HTMLInputElement>();
const { searchFilter, showSearch, resetSearch } = useSearchFilter();
onMounted(() => {
hotkeys("command+f, ctrl+f", (event, handler) => {
showSearch.value = true;
nextTick(() => input.value?.focus() || input.value?.select());
event.preventDefault();
});
hotkeys("esc", () => resetSearch());
});
onUnmounted(() => {
searchFilter.value = "";
showSearch.value = false;
hotkeys.unbind("command+f, ctrl+f");
hotkeys.unbind("esc");
});
</script>
<style lang="scss" scoped>

View File

@@ -9,21 +9,17 @@
</router-link>
</div>
<div class="column is-narrow has-text-right px-1">
<button
class="button is-small is-rounded is-settings-control pl-1 pr-1"
@click="$emit('search')"
title="Search containers (⌘ + k, ⌃k)"
>
<search-icon />
<button class="button is-rounded" @click="$emit('search')" title="Search containers (⌘ + k, ⌃k)">
<span class="icon">
<mdi-light-magnify />
</span>
</button>
</div>
<div class="column is-narrow has-text-right px-0">
<router-link
:to="{ name: 'settings' }"
active-class="is-active"
class="button is-small is-rounded is-settings-control pl-1 pr-1"
>
<settings-icon />
<router-link :to="{ name: 'settings' }" active-class="is-active" class="button is-rounded">
<span class="icon">
<mdi-light-cog />
</span>
</router-link>
</div>
</div>
@@ -42,11 +38,11 @@
<div class="is-flex-shrink-1 column-icon">
<span
class="icon is-small"
@click.stop.prevent="appendActiveContainer(item)"
@click.stop.prevent="store.appendActiveContainer(item)"
v-show="!activeContainersById[item.id]"
title="Pin as column"
>
<columns-icon />
<cil-columns />
</span>
</div>
</div>
@@ -56,39 +52,22 @@
</aside>
</template>
<script>
import { mapActions, mapGetters, mapState } from "vuex";
<script lang="ts" setup>
import { computed } from "vue";
import { storeToRefs } from "pinia";
import { useContainerStore } from "@/stores/container";
import type { Container } from "@/types/Container";
import SearchIcon from "~icons/mdi-light/magnify";
import SettingsIcon from "~icons/mdi-light/cog";
import ColumnsIcon from "~icons/cil/columns";
const store = useContainerStore();
export default {
props: [],
name: "SideMenu",
components: {
SearchIcon,
SettingsIcon,
ColumnsIcon,
},
data() {
return {};
},
computed: {
...mapGetters(["visibleContainers", "activeContainers"]),
activeContainersById() {
return this.activeContainers.reduce((map, obj) => {
map[obj.id] = obj;
return map;
}, {});
},
},
methods: {
...mapActions({
appendActiveContainer: "APPEND_ACTIVE_CONTAINER",
}),
},
};
const { activeContainers, visibleContainers } = storeToRefs(store);
const activeContainersById = computed(() =>
activeContainers.value.reduce((acc, item) => {
acc[item.id] = item;
return acc;
}, {} as Record<string, Container>)
);
</script>
<style scoped lang="scss">
aside {

View File

@@ -1,30 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<LogEventSource /> renders correctly 1`] = `
<div>
<div
class="infinte-loader"
>
<div
class="spinner"
style="display: none;"
>
<div
class="bounce1"
/>
<div
class="bounce2"
/>
<div
class="bounce3"
/>
</div>
</div>
<ul
class="events medium"
/>
</div>
`;

View File

@@ -0,0 +1,87 @@
// Vitest Snapshot v1
exports[`<LogEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
"<ul class=\\"events medium\\" data-v-28f125ea=\\"\\">
<li data-key=\\"2019-06-12T23:55:42.459034602Z\\" class=\\"\\" data-v-28f125ea=\\"\\">
<!--v-if-->
<div class=\\"line\\" data-v-28f125ea=\\"\\"><span class=\\"date\\" data-v-28f125ea=\\"\\"><time datetime=\\"2019-06-12T23:55:42.459Z\\" data-v-28f125ea=\\"\\">today at 11:55:42 PM</time></span><span class=\\"text\\" data-v-28f125ea=\\"\\">&lt;test&gt;foo bar&lt;/test&gt;</span></div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render dates with 24 hour style 1`] = `
"<ul class=\\"events medium\\" data-v-28f125ea=\\"\\">
<li data-key=\\"2019-06-12T23:55:42.459034602Z\\" class=\\"\\" data-v-28f125ea=\\"\\">
<!--v-if-->
<div class=\\"line\\" data-v-28f125ea=\\"\\"><span class=\\"date\\" data-v-28f125ea=\\"\\"><time datetime=\\"2019-06-12T23:55:42.459Z\\" data-v-28f125ea=\\"\\">today at 23:55:42</time></span><span class=\\"text\\" data-v-28f125ea=\\"\\">&lt;test&gt;foo bar&lt;/test&gt;</span></div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages 1`] = `
"<ul class=\\"events medium\\" data-v-28f125ea=\\"\\">
<li data-key=\\"2019-06-12T10:55:42.459034602Z\\" class=\\"\\" data-v-28f125ea=\\"\\">
<!--v-if-->
<div class=\\"line\\" data-v-28f125ea=\\"\\"><span class=\\"date\\" data-v-28f125ea=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-28f125ea=\\"\\">today at 10:55:42 AM</time></span><span class=\\"text\\" data-v-28f125ea=\\"\\">\\"This is a message.\\"</span></div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with color 1`] = `
"<ul class=\\"events medium\\" data-v-28f125ea=\\"\\">
<li data-key=\\"2019-06-12T10:55:42.459034602Z\\" class=\\"\\" data-v-28f125ea=\\"\\">
<!--v-if-->
<div class=\\"line\\" data-v-28f125ea=\\"\\"><span class=\\"date\\" data-v-28f125ea=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-28f125ea=\\"\\">today at 10:55:42 AM</time></span><span class=\\"text\\" data-v-28f125ea=\\"\\"><span style=\\"color:#000\\">black<span style=\\"color:#AAA\\">white</span></span></span></div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with filter 1`] = `
"<ul class=\\"events medium\\" data-v-28f125ea=\\"\\">
<li data-key=\\"2019-06-12T10:55:42.459034602Z\\" class=\\"\\" data-v-28f125ea=\\"\\">
<!--v-if-->
<div class=\\"line\\" data-v-28f125ea=\\"\\"><span class=\\"date\\" data-v-28f125ea=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-28f125ea=\\"\\">today at 10:55:42 AM</time></span><span class=\\"text\\" data-v-28f125ea=\\"\\">This is a <mark>test</mark> &lt;hi&gt;&lt;/hi&gt;</span></div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with html entities 1`] = `
"<ul class=\\"events medium\\" data-v-28f125ea=\\"\\">
<li data-key=\\"2019-06-12T10:55:42.459034602Z\\" class=\\"\\" data-v-28f125ea=\\"\\">
<!--v-if-->
<div class=\\"line\\" data-v-28f125ea=\\"\\"><span class=\\"date\\" data-v-28f125ea=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-28f125ea=\\"\\">today at 10:55:42 AM</time></span><span class=\\"text\\" data-v-28f125ea=\\"\\">&lt;test&gt;foo bar&lt;/test&gt;</span></div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > renders correctly 1`] = `
"<div class=\\"infinte-loader\\" data-v-48dce4fc=\\"\\">
<div class=\\"spinner\\" data-v-48dce4fc=\\"\\" style=\\"display: none;\\">
<div class=\\"bounce1\\" data-v-48dce4fc=\\"\\"></div>
<div class=\\"bounce2\\" data-v-48dce4fc=\\"\\"></div>
<div class=\\"bounce3\\" data-v-48dce4fc=\\"\\"></div>
</div>
</div>
<ul class=\\"events medium\\" data-v-28f125ea=\\"\\"></ul>"
`;
exports[`<LogEventSource /> > should parse messages 1`] = `
{
"date": 2019-06-12T10:55:42.459Z,
"message": "\\"This is a message.\\"",
}
`;
exports[`<LogEventSource /> > should parse messages with loki's timestamp format 1`] = `
{
"date": 2020-04-27T10:35:43.272Z,
"message": "xxxxx",
}
`;
exports[`<LogEventSource /> > should pass messages to slot 1`] = `
{
"date": 2019-06-12T10:55:42.459Z,
"message": "\\"This is a message.\\"",
}
`;

View File

@@ -1,19 +0,0 @@
import { mapGetters } from "vuex";
export default {
computed: {
...mapGetters(["allContainersById"]),
container() {
return this.allContainersById[this.id];
},
},
watch: {
["container.state"](newValue, oldValue) {
if (newValue == "running" && newValue != oldValue) {
this.onContainerStateChange(newValue, oldValue);
}
},
},
methods: {
onContainerStateChange(newValue, oldValue) {},
},
};

View File

@@ -0,0 +1,3 @@
import { useMediaQuery } from "@vueuse/core";
export const isMobile = useMediaQuery("(max-width: 770px)");

View File

@@ -0,0 +1,50 @@
import { ref, computed, Ref } from "vue";
const searchFilter = ref<string>();
const showSearch = ref(false);
import type { LogEntry } from "@/types/LogEntry";
export function useSearchFilter() {
function filteredMessages(messages: Ref<LogEntry[]>) {
return computed(() => {
if (searchFilter && searchFilter.value) {
const isSmartCase = searchFilter.value === searchFilter.value.toLowerCase();
try {
const regex = isSmartCase ? new RegExp(searchFilter.value, "i") : new RegExp(searchFilter.value);
return messages.value
.filter((d) => d.message.match(regex))
.map((d) => ({
...d,
message: d.message.replace(regex, "<mark>$&</mark>"),
}));
} catch (e) {
if (e instanceof SyntaxError) {
console.info(`Ignoring SytaxError from search.`, e);
return messages.value;
}
throw e;
}
}
return messages.value;
});
}
function resetSearch() {
searchFilter.value = "";
showSearch.value = false;
}
function isSearching() {
return showSearch.value && searchFilter.value;
}
return {
filteredMessages,
searchFilter,
showSearch,
resetSearch,
isSearching,
};
}

View File

@@ -0,0 +1,65 @@
import { useStorage } from "@vueuse/core";
import { computed } from "vue";
export const DOZZLE_SETTINGS_KEY = "DOZZLE_SETTINGS";
export const DEFAULT_SETTINGS: {
search: boolean;
size: "small" | "medium" | "large";
menuWidth: number;
smallerScrollbars: boolean;
showTimestamp: boolean;
showAllContainers: boolean;
lightTheme: "auto" | "dark" | "light";
hourStyle: "auto" | "24" | "12";
} = {
search: true,
size: "medium",
menuWidth: 15,
smallerScrollbars: false,
showTimestamp: true,
showAllContainers: false,
lightTheme: "auto",
hourStyle: "auto",
};
export const settings = useStorage(DOZZLE_SETTINGS_KEY, DEFAULT_SETTINGS);
export const search = computed({
get: () => settings.value.search,
set: (value) => (settings.value.search = value),
});
export const size = computed({
get: () => settings.value.size,
set: (value) => (settings.value.size = value),
});
export const menuWidth = computed({
get: () => settings.value.menuWidth,
set: (value) => (settings.value.menuWidth = value),
});
export const smallerScrollbars = computed({
get: () => settings.value.smallerScrollbars,
set: (value) => (settings.value.smallerScrollbars = value),
});
export const showTimestamp = computed({
get: () => settings.value.showTimestamp,
set: (value) => (settings.value.showTimestamp = value),
});
export const showAllContainers = computed({
get: () => settings.value.showAllContainers,
set: (value) => (settings.value.showAllContainers = value),
});
export const lightTheme = computed({
get: () => settings.value.lightTheme,
set: (value) => (settings.value.lightTheme = value),
});
export const hourStyle = computed({
get: () => settings.value.hourStyle,
set: (value) => (settings.value.hourStyle = value),
});

View File

@@ -0,0 +1,12 @@
import { useTitle } from "@vueuse/core";
import { ref, computed } from "vue";
const subtitle = ref("");
const title = computed(() => `${subtitle.value} - Dozzle`);
useTitle(title);
export function setTitle(t: string) {
subtitle.value = t;
}

View File

@@ -1,74 +0,0 @@
import Vue from "vue";
import VueRouter from "vue-router";
import Meta from "vue-meta";
import Switch from "buefy/dist/esm/switch";
import Radio from "buefy/dist/esm/radio";
import Field from "buefy/dist/esm/field";
import Modal from "buefy/dist/esm/modal";
import Tooltip from "buefy/dist/esm/tooltip";
import Autocomplete from "buefy/dist/esm/autocomplete";
import store from "./store";
import config from "./store/config";
import App from "./App.vue";
import { Container, Settings, Index, Show, ContainerNotFound, PageNotFound, Login } from "./pages";
Vue.use(VueRouter);
Vue.use(Meta);
Vue.use(Switch);
Vue.use(Radio);
Vue.use(Field);
Vue.use(Modal);
Vue.use(Tooltip);
Vue.use(Autocomplete);
const routes = [
{
path: "/",
component: Index,
name: "default",
},
{
path: "/container/:id",
component: Container,
name: "container",
props: true,
},
{
path: "/container/*",
component: ContainerNotFound,
name: "container-not-found",
},
{
path: "/settings",
component: Settings,
name: "settings",
},
{
path: "/show",
component: Show,
name: "show",
},
{
path: "/login",
component: Login,
name: "login",
},
{
path: "/*",
component: PageNotFound,
name: "page-not-found",
},
];
const router = new VueRouter({
mode: "history",
base: config.base + "/",
routes,
});
new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#app");

67
assets/main.ts Normal file
View File

@@ -0,0 +1,67 @@
import "./styles.scss";
import { createApp } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import { Autocomplete, Button, Dropdown, Switch, Radio, Field, Tooltip, Modal, Config } from "@oruga-ui/oruga-next";
import { bulmaConfig } from "@oruga-ui/theme-bulma";
import { createPinia } from "pinia";
import config from "./stores/config";
import App from "./App.vue";
import { Container, Settings, Index, Show, ContainerNotFound, PageNotFound, Login } from "./pages";
const routes = [
{
path: "/",
component: Index,
name: "default",
},
{
path: "/container/:id",
component: Container,
name: "container",
props: true,
},
{
path: "/container/:pathMatch(.*)",
component: ContainerNotFound,
name: "container-not-found",
},
{
path: "/settings",
component: Settings,
name: "settings",
},
{
path: "/show",
component: Show,
name: "show",
},
{
path: "/login",
component: Login,
name: "login",
},
{
path: "/:pathMatch(.*)*",
component: PageNotFound,
name: "page-not-found",
},
];
const router = createRouter({
history: createWebHistory(`${config.base}/`),
routes,
});
createApp(App)
.use(router)
.use(createPinia())
.use(Autocomplete)
.use(Button)
.use(Dropdown)
.use(Switch)
.use(Tooltip)
.use(Modal)
.use(Radio)
.use(Field)
.use(Config, bulmaConfig)
.mount("#app");

View File

@@ -5,43 +5,33 @@
</div>
</template>
<script>
import { mapGetters } from "vuex";
import Search from "../components/Search";
import LogContainer from "../components/LogContainer";
<script lang="ts" setup>
import { onMounted, toRefs, watchEffect } from "vue";
import Search from "@/components/Search.vue";
import LogContainer from "@/components/LogContainer.vue";
import { setTitle } from "@/composables/title";
import { useContainerStore } from "@/stores/container";
import { storeToRefs } from "pinia";
export default {
props: ["id"],
name: "Container",
components: {
LogContainer,
Search,
const store = useContainerStore();
const props = defineProps({
id: {
type: String,
required: true,
},
data() {
return {
title: "loading",
};
},
metaInfo() {
return {
title: this.title,
};
},
mounted() {
if (this.allContainersById[this.id]) {
this.title = this.allContainersById[this.id].name;
}
},
computed: {
...mapGetters(["allContainersById", "activeContainers"]),
},
watch: {
id() {
this.title = this.allContainersById[this.id].name;
},
allContainersById() {
this.title = this.allContainersById[this.id].name;
},
},
};
});
const { id } = toRefs(props);
const currentContainer = store.currentContainer(id);
const { activeContainers } = storeToRefs(store);
setTitle("loading");
onMounted(() => {
setTitle(currentContainer.value?.name);
});
watchEffect(() => setTitle(currentContainer.value?.name));
</script>

View File

@@ -11,13 +11,13 @@
</div>
</template>
<script>
<script lang="ts">
import { setTitle } from "@/composables/title";
export default {
name: "ContainerNotFound",
metaInfo() {
return {
title: "Not Found",
};
setup() {
setTitle("Container not found");
},
};
</script>

View File

@@ -76,60 +76,47 @@
</div>
</template>
<script>
import { mapState } from "vuex";
import SearchIcon from "~icons/mdi-light/magnify";
import PastTime from "../components/PastTime";
import config from "../store/config";
<script lang="ts" setup>
import { ref, computed } from "vue";
import { storeToRefs } from "pinia";
import { useRouter } from "vue-router";
import { useContainerStore } from "@/stores/container";
import fuzzysort from "fuzzysort";
import SearchIcon from "~icons/mdi-light/magnify";
import PastTime from "../components/PastTime.vue";
import config from "@/stores/config";
export default {
name: "Index",
components: { SearchIcon, PastTime },
data() {
return {
version: config.version,
search: null,
sort: "running",
secured: config.secured,
base: config.base,
};
},
methods: {
onEnter() {
if (this.results.length == 1) {
const [item] = this.results;
this.$router.push({ name: "container", params: { id: item.id, name: item.name } });
}
},
},
computed: {
...mapState(["containers"]),
mostRecentContainers() {
return [...this.containers].sort((a, b) => b.created - a.created);
},
runningContainers() {
return this.mostRecentContainers.filter((c) => c.state === "running");
},
allContainers() {
return this.containers;
},
results() {
if (this.search) {
return fuzzysort.go(this.search, this.allContainers, { key: "name" }).map((i) => i.obj);
}
switch (this.sort) {
case "all":
return this.mostRecentContainers;
case "running":
return this.runningContainers;
const { base, version, secured } = config;
const containerStore = useContainerStore();
const { containers } = storeToRefs(containerStore);
const router = useRouter();
default:
throw `Invalid sort order: ${this.sort}`;
}
},
},
};
const sort = ref("running");
const search = ref();
const results = computed(() => {
if (search.value) {
return fuzzysort.go(search.value, containers.value, { key: "name" }).map((i) => i.obj);
}
switch (sort.value) {
case "all":
return mostRecentContainers.value;
case "running":
return runningContainers.value;
default:
throw `Invalid sort order: ${sort.value}`;
}
});
const mostRecentContainers = computed(() => [...containers.value].sort((a, b) => b.created - a.created));
const runningContainers = computed(() => mostRecentContainers.value.filter((c) => c.state === "running"));
function onEnter() {
if (results.value.length == 1) {
const [item] = results.value;
router.push({ name: "container", params: { id: item.id, name: item.name } });
}
}
</script>
<style lang="scss" scoped>
.panel {

View File

@@ -49,8 +49,9 @@
</div>
</template>
<script>
import config from "../store/config";
<script lang="ts">
import config from "@/stores/config";
import { setTitle } from "@/composables/title";
export default {
name: "Login",
data() {
@@ -60,10 +61,8 @@ export default {
error: false,
};
},
metaInfo() {
return {
title: "Authentication Required",
};
setup() {
setTitle("Authentication Required");
},
methods: {
async onLogin() {

View File

@@ -3,21 +3,20 @@
<div class="hero-body">
<div class="container has-text-centered">
<h1 class="title">
Oops,
<small class="subtitle">this page doesn't exist</small>
404.
<small class="subtitle">This page does not exist.</small>
</h1>
</div>
</div>
</div>
</template>
<script>
<script lang="ts">
import { setTitle } from "@/composables/title";
export default {
name: "PageNotFound",
metaInfo() {
return {
title: "404 Error",
};
setup() {
setTitle("Page not found");
},
};
</script>

View File

@@ -10,9 +10,8 @@
>.
<span v-if="hasUpdate">
New version is available! Update to
<a :href="nextRelease.html_url" class="next-release" target="_blank" rel="noreferrer noopener">{{
nextRelease.name
}}</a
<a :href="nextRelease.html_url" class="next-release" target="_blank" rel="noreferrer noopener">
{{ nextRelease.name }}</a
>.
</span>
</div>
@@ -25,16 +24,22 @@
<div class="item">
<div class="columns is-vcentered">
<div class="column is-narrow">
<b-field>
<b-radio-button
v-model="hourStyle"
:native-value="value"
v-for="value in ['auto', '12', '24']"
:key="value"
>
<span class="is-capitalized">{{ value }}</span>
</b-radio-button>
</b-field>
<o-field>
<o-dropdown v-model="hourStyle" aria-role="list">
<template #trigger>
<o-button variant="primary" type="button">
<span class="is-capitalized">{{ hourStyle }}</span>
<span class="icon">
<carbon-caret-down />
</span>
</o-button>
</template>
<o-dropdown-item :value="value" aria-role="listitem" v-for="value in ['auto', '12', '24']" :key="value">
<span class="is-capitalized">{{ value }}</span>
</o-dropdown-item>
</o-dropdown>
</o-field>
</div>
<div class="column">
By default, Dozzle will use your browser's locale to format time. You can force to 12 or 24 hour style.
@@ -42,30 +47,69 @@
</div>
<div class="item">
<b-switch v-model="smallerScrollbars"> Use smaller scrollbars </b-switch>
<o-switch v-model="smallerScrollbars"> Use smaller scrollbars </o-switch>
</div>
<div class="item">
<b-switch v-model="showTimestamp"> Show timestamps </b-switch>
<o-switch v-model="showTimestamp"> Show timestamps </o-switch>
</div>
</div>
<div class="item">
<div class="columns is-vcentered">
<div class="column is-narrow">
<b-field>
<b-radio-button
v-model="size"
:native-value="value"
v-for="value in ['small', 'medium', 'large']"
:key="value"
>
<span class="is-capitalized">{{ value }}</span>
</b-radio-button>
</b-field>
<o-field>
<o-dropdown v-model="size" aria-role="list">
<template #trigger>
<o-button variant="primary" type="button">
<span class="is-capitalized">{{ size }}</span>
<span class="icon">
<carbon-caret-down />
</span>
</o-button>
</template>
<o-dropdown-item
:value="value"
aria-role="listitem"
v-for="value in ['small', 'medium', 'large']"
:key="value"
>
<span class="is-capitalized">{{ value }}</span>
</o-dropdown-item>
</o-dropdown>
</o-field>
</div>
<div class="column">Font size to use for logs</div>
</div>
</div>
<div class="item">
<div class="columns is-vcentered">
<div class="column is-narrow">
<o-field>
<o-dropdown v-model="lightTheme" aria-role="list">
<template #trigger>
<o-button variant="primary" type="button">
<span class="is-capitalized">{{ lightTheme }}</span>
<span class="icon">
<carbon-caret-down />
</span>
</o-button>
</template>
<o-dropdown-item
:value="value"
aria-role="listitem"
v-for="value in ['auto', 'dark', 'light']"
:key="value"
>
<span class="is-capitalized">{{ value }}</span>
</o-dropdown-item>
</o-dropdown>
</o-field>
</div>
<div class="column">Color scheme</div>
</div>
</div>
</section>
<section class="section">
<div class="has-underline">
@@ -73,74 +117,53 @@
</div>
<div class="item">
<b-switch v-model="search">
<o-switch v-model="search">
Enable searching with Dozzle using <code>command+f</code> or <code>ctrl+f</code>
</b-switch>
</o-switch>
</div>
<div class="item">
<b-switch v-model="showAllContainers"> Show stopped containers </b-switch>
</div>
<div class="item">
<b-switch v-model="lightTheme"> Use light theme </b-switch>
<o-switch v-model="showAllContainers"> Show stopped containers </o-switch>
</div>
</section>
</div>
</template>
<script>
<script lang="ts" setup>
import { ref } from "vue";
import gt from "semver/functions/gt";
import { mapActions, mapState } from "vuex";
import config from "../store/config";
import config from "@/stores/config";
import { setTitle } from "@/composables/title";
import {
search,
lightTheme,
smallerScrollbars,
showTimestamp,
hourStyle,
showAllContainers,
size,
} from "@/composables/settings";
export default {
props: [],
name: "Settings",
data() {
return {
currentVersion: config.version,
nextRelease: null,
hasUpdate: false,
};
},
async created() {
const releases = await (await fetch("https://api.github.com/repos/amir20/dozzle/releases")).json();
if (this.currentVersion !== "master") {
this.hasUpdate = gt(releases[0].tag_name, this.currentVersion);
} else {
this.hasUpdate = true;
setTitle("Settings");
const currentVersion = config.version;
const nextRelease = ref({ html_url: "", name: "" });
const hasUpdate = ref(false);
async function fetchNextRelease() {
if (!["dev", "master"].includes(currentVersion)) {
const response = await fetch("https://api.github.com/repos/dozzle/dozzle/releases/latest");
if (response.ok) {
const releases = await response.json();
hasUpdate.value = gt(releases[0].tag_name, currentVersion);
nextRelease.value = releases[0];
}
this.nextRelease = releases[0];
},
metaInfo() {
return {
title: "Settings",
};
},
methods: {
...mapActions({
updateSetting: "UPDATE_SETTING",
}),
},
computed: {
...mapState(["settings"]),
...["search", "size", "smallerScrollbars", "showTimestamp", "showAllContainers", "lightTheme", "hourStyle"].reduce(
(map, name) => {
map[name] = {
get() {
return this.settings[name];
},
set(value) {
this.updateSetting({ [name]: value });
},
};
return map;
},
{}
),
},
};
} else {
hasUpdate.value = true;
}
}
fetchNextRelease();
</script>
<style lang="scss" scoped>
.title {

View File

@@ -1,29 +1,29 @@
<template></template>
<script lang="ts" setup>
import { useContainerStore } from "@/stores/container";
import { storeToRefs } from "pinia";
import { watch } from "vue";
import { useRoute, useRouter } from "vue-router";
<script>
import { mapActions, mapGetters, mapState } from "vuex";
export default {
props: [],
name: "Show",
computed: mapGetters(["visibleContainers"]),
watch: {
visibleContainers(newValue) {
if (newValue) {
if (this.$route.query.name) {
const [container, _] = this.visibleContainers.filter((c) => c.name == this.$route.query.name);
if (container) {
this.$router.push({ name: "container", params: { id: container.id } });
} else {
console.error(`No containers found matching name=${this.$route.query.name}. Redirecting to /`);
this.$router.push({ name: "default" });
}
} else {
console.error(`Expection query parameter name to be set. Redirecting to /`);
this.$router.push({ name: "default" });
}
const router = useRouter();
const route = useRoute();
const store = useContainerStore();
const { visibleContainers } = storeToRefs(store);
watch(visibleContainers, (newValue) => {
if (newValue) {
if (route.query.name) {
const [container, _] = visibleContainers.value.filter((c) => c.name == route.query.name);
if (container) {
router.push({ name: "container", params: { id: container.id } });
} else {
console.error(`No containers found matching name=${route.query.name}. Redirecting to /`);
router.push({ name: "default" });
}
},
},
};
} else {
console.error(`Expection query parameter name to be set. Redirecting to /`);
router.push({ name: "default" });
}
}
});
</script>
<style scoped></style>

6
assets/shims-vue.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/* eslint-disable */
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}

View File

@@ -1,120 +0,0 @@
import Vue from "vue";
import Vuex from "vuex";
import storage from "store/dist/store.modern";
import { DEFAULT_SETTINGS, DOZZLE_SETTINGS_KEY } from "./settings";
import config from "./config";
Vue.use(Vuex);
const mql = window.matchMedia("(max-width: 770px)");
storage.set(DOZZLE_SETTINGS_KEY, { ...DEFAULT_SETTINGS, ...storage.get(DOZZLE_SETTINGS_KEY) });
const state = {
containers: [],
activeContainerIds: [],
searchFilter: null,
isMobile: mql.matches,
settings: storage.get(DOZZLE_SETTINGS_KEY),
authorizationNeeded: config.authorizationNeeded,
};
const mutations = {
SET_CONTAINERS(state, containers) {
const containersById = getters.allContainersById({ containers });
containers.forEach((container) => {
container.stat =
containersById[container.id] && containersById[container.id].stat
? containersById[container.id].stat
: { memoryUsage: 0, cpu: 0 };
});
state.containers = containers;
},
ADD_ACTIVE_CONTAINERS(state, { id }) {
state.activeContainerIds.push(id);
},
REMOVE_ACTIVE_CONTAINER(state, { id }) {
state.activeContainerIds.splice(state.activeContainerIds.indexOf(id), 1);
},
SET_SEARCH(state, filter) {
state.searchFilter = filter;
},
SET_MOBILE_WIDTH(state, value) {
state.isMobile = value;
},
UPDATE_SETTINGS(state, newValues) {
state.settings = { ...state.settings, ...newValues };
storage.set(DOZZLE_SETTINGS_KEY, state.settings);
},
UPDATE_CONTAINER(_, { container, data }) {
for (const [key, value] of Object.entries(data)) {
Vue.set(container, key, value);
}
},
};
const actions = {
APPEND_ACTIVE_CONTAINER({ commit }, container) {
commit("ADD_ACTIVE_CONTAINERS", container);
},
REMOVE_ACTIVE_CONTAINER({ commit }, container) {
commit("REMOVE_ACTIVE_CONTAINER", container);
},
SET_SEARCH({ commit }, filter) {
commit("SET_SEARCH", filter);
},
UPDATE_SETTING({ commit }, setting) {
commit("UPDATE_SETTINGS", setting);
},
UPDATE_STATS({ commit, getters: { allContainersById } }, stat) {
const container = allContainersById[stat.id];
if (container) {
commit("UPDATE_CONTAINER", { container, data: { stat } });
}
},
UPDATE_CONTAINER({ commit, getters: { allContainersById } }, event) {
switch (event.name) {
case "die":
const container = allContainersById[event.actorId];
commit("UPDATE_CONTAINER", { container, data: { state: "exited" } });
break;
default:
}
},
};
const getters = {
allContainersById({ containers }) {
return containers.reduce((map, obj) => {
map[obj.id] = obj;
return map;
}, {});
},
visibleContainers({ containers, settings: { showAllContainers } }) {
const filter = showAllContainers ? () => true : (c) => c.state === "running";
return containers.filter(filter);
},
activeContainers({ activeContainerIds }, { allContainersById }) {
return activeContainerIds.map((id) => allContainersById[id]);
},
};
if (!config.authorizationNeeded) {
const es = new EventSource(`${config.base}/api/events/stream`);
es.addEventListener("containers-changed", (e) => store.commit("SET_CONTAINERS", JSON.parse(e.data)), false);
es.addEventListener("container-stat", (e) => store.dispatch("UPDATE_STATS", JSON.parse(e.data)), false);
es.addEventListener("container-die", (e) => store.dispatch("UPDATE_CONTAINER", JSON.parse(e.data)), false);
}
mql.addEventListener("change", (e) => store.commit("SET_MOBILE_WIDTH", e.matches));
const store = new Vuex.Store({
state,
getters,
actions,
mutations,
});
export default store;

View File

@@ -1,11 +0,0 @@
export const DOZZLE_SETTINGS_KEY = "DOZZLE_SETTINGS";
export const DEFAULT_SETTINGS = {
search: true,
size: "medium",
menuWidth: 15,
smallerScrollbars: false,
showTimestamp: true,
showAllContainers: false,
lightTheme: false,
hourStyle: "auto",
};

View File

@@ -1,4 +1,6 @@
const config = JSON.parse(document.querySelector("script#config__json").textContent);
const text = document.querySelector("script#config__json")?.textContent || "{}";
const config = JSON.parse(text);
if (config.version == "{{ .Version }}") {
config.version = "master";
config.base = "";
@@ -9,5 +11,4 @@ if (config.version == "{{ .Version }}") {
config.authorizationNeeded = config.authorizationNeeded === "true";
config.secured = config.secured === "true";
}
export default config;

View File

@@ -0,0 +1,66 @@
import { acceptHMRUpdate, defineStore } from "pinia";
import { ref, Ref, computed } from "vue";
import { showAllContainers } from "@/composables/settings";
import config from "@/stores/config";
import type { Container, ContainerStat } from "@/types/Container";
export const useContainerStore = defineStore("container", () => {
const containers = ref<Container[]>([]);
const activeContainerIds = ref<string[]>([]);
const allContainersById = computed(() =>
containers.value.reduce((acc, container) => {
acc[container.id] = container;
return acc;
}, {} as Record<string, Container>)
);
const visibleContainers = computed(() => {
const filter = showAllContainers.value ? () => true : (c: Container) => c.state === "running";
return containers.value.filter(filter);
});
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) => (containers.value = JSON.parse((e as MessageEvent).data)),
false
);
es.addEventListener(
"container-stat",
(e) => {
const stat = JSON.parse((e as MessageEvent).data) as ContainerStat;
const container = allContainersById.value[stat.id];
if (container) {
container.stat = stat;
}
},
false
);
// es.addEventListener("container-die", (e) => store.dispatch("UPDATE_CONTAINER", JSON.parse(e.data)), false);
const currentContainer = (id: Ref<string>) => computed(() => allContainersById.value[id.value]);
const appendActiveContainer = ({ id }: Container) => activeContainerIds.value.push(id);
const removeActiveContainer = ({ id }: Container) =>
activeContainerIds.value.splice(activeContainerIds.value.indexOf(id), 1);
return {
containers,
activeContainerIds,
allContainersById,
visibleContainers,
activeContainers,
currentContainer,
appendActiveContainer,
removeActiveContainer,
};
});
// @ts-ignore
if (import.meta.hot) {
// @ts-ignore
import.meta.hot.accept(acceptHMRUpdate(useContainerStore, import.meta.hot));
}

View File

@@ -1,5 +1,5 @@
@charset "utf-8";
@import "~bulma/sass/utilities/initial-variables.sass";
@import "bulma/sass/utilities/initial-variables.sass";
$body-background-color: var(--body-background-color);
@@ -27,16 +27,18 @@ $link-active: $grey-dark;
$dark-toolbar-color: rgba($black-bis, 0.7);
$light-toolbar-color: rgba($grey-darker, 0.7);
@import "~bulma";
@import "../node_modules/splitpanes/dist/splitpanes.css";
@import "~buefy/src/scss/utils/_all";
@import "~buefy/src/scss/components/_switch";
@import "~buefy/src/scss/components/_radio";
@import "~buefy/src/scss/components/_modal";
@import "~buefy/src/scss/components/_tooltip";
@import "~buefy/src/scss/components/_autocomplete";
@import "bulma/bulma.sass";
@import "@oruga-ui/theme-bulma/dist/scss/components/utils/all.scss";
@import "@oruga-ui/theme-bulma/dist/scss/components/autocomplete.scss";
@import "@oruga-ui/theme-bulma/dist/scss/components/button.scss";
@import "@oruga-ui/theme-bulma/dist/scss/components/modal.scss";
@import "@oruga-ui/theme-bulma/dist/scss/components/switch.scss";
@import "@oruga-ui/theme-bulma/dist/scss/components/tooltip.scss";
@import "@oruga-ui/theme-bulma/dist/scss/components/dropdown.scss";
@import "splitpanes/dist/splitpanes.css";
html {
html,
[data-theme="dark"] {
--scheme-main: #{$black};
--scheme-main-bis: #{$black-bis};
--scheme-main-ter: #{$black-ter};
@@ -63,6 +65,64 @@ html {
--text-color: #{$grey-lighter};
}
@media (prefers-color-scheme: dark) {
html {
--scheme-main: #{$black};
--scheme-main-bis: #{$black-bis};
--scheme-main-ter: #{$black-ter};
--border-color: #{$grey-darker};
--border-hover-color: var(--secondary-color);
--logo-color: var(--secondary-color);
--primary-color: #{$turquoise};
--secondary-color: #{$yellow};
--body-background-color: #{$black-bis};
--action-toolbar-background-color: #{$dark-toolbar-color};
--menu-item-active-background-color: var(--primary-color);
--menu-item-color: hsl(0, 6%, 87%);
--menu-item-hover-background-color: #{$white-ter};
--menu-item-hover-color: #{$black-ter};
--panel-heading-background-color: var(--secondary-color);
--panel-heading-color: var(--scheme-main-bis);
--text-strong-color: #{$grey-lightest};
--text-color: #{$grey-lighter};
}
}
@media (prefers-color-scheme: light) {
html {
--scheme-main: #{$white};
--scheme-main-bis: #{$white-bis};
--scheme-main-ter: #{$white-ter};
--border-color: #{$grey-lighter};
--border-hover-color: var(--secondary-color);
--logo-color: #{$grey-darker};
--primary-color: #{$turquoise};
--secondary-color: #d8f0ca;
--body-background-color: #{$white-bis};
--action-toolbar-background-color: #{$light-toolbar-color};
--body-color: #{$grey-darker};
--menu-item-color: #{$grey-dark};
--menu-item-hover-background-color: #eee8e7;
--menu-item-hover-color: #{black-ter};
--panel-heading-background-color: var(--secondary-color);
--panel-heading-color: var(--text-strong-color);
--text-strong-color: #{$grey-dark};
--text-color: #{$grey-darker};
}
}
[data-theme="light"] {
--scheme-main: #{$white};
--scheme-main-bis: #{$white-bis};
@@ -126,22 +186,6 @@ html.has-custom-scrollbars {
}
}
.is-settings-control {
background: rgba(0, 0, 0, 0.4);
color: #fff;
border-color: transparent;
&:hover {
border-color: var(--border-hover-color) !important;
background: rgba(0, 0, 0, 0.8) !important;
color: #fff !important;
}
&:focus {
box-shadow: none !important;
color: unset;
border-color: transparent;
}
}
@media screen and (min-width: 770px) {
.splitpanes__pane {
overflow: unset;
@@ -161,3 +205,7 @@ html.has-custom-scrollbars {
.modal {
z-index: 1000;
}
.button .button-wrapper > span {
display: contents;
}

16
assets/types/Container.d.ts vendored Normal file
View File

@@ -0,0 +1,16 @@
export interface Container {
readonly id: string;
readonly created: number;
readonly image: string;
readonly name: string;
readonly state: "created" | "running" | "exited" | "dead" | "paused" | "restarting";
readonly status: string;
stat?: ContainerStat;
}
export interface ContainerStat {
readonly id: string;
readonly cpu: number;
readonly memory: number;
readonly memoryUsage: number;
}

6
assets/types/LogEntry.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
export interface LogEntry {
date: Date;
message: string;
key: string;
event?: string;
}

View File

@@ -75,8 +75,8 @@ func (d *dockerClient) FindContainer(id string) (Container, error) {
break
}
}
if found == false {
return container, fmt.Errorf("Unable to find container with id: %s", id)
if !found {
return container, fmt.Errorf("unable to find container with id: %s", id)
}
return container, nil
@@ -174,6 +174,7 @@ func (d *dockerClient) ContainerLogs(ctx context.Context, id string, tailSize in
Since: since,
}
log.Debugf("streaming logs from Docker with option: %+v", options)
reader, err := d.cli.ContainerLogs(ctx, id, options)
if err != nil {
return nil, err
@@ -221,10 +222,12 @@ func (d *dockerClient) ContainerLogsBetweenDates(ctx context.Context, id string,
ShowStdout: true,
ShowStderr: true,
Timestamps: true,
Since: strconv.FormatInt(from.Unix(), 10),
Until: strconv.FormatInt(to.Unix(), 10),
Since: from.Format(time.RFC3339),
Until: to.Format(time.RFC3339),
}
log.Debugf("fetching logs from Docker with option: %+v", options)
reader, err := d.cli.ContainerLogs(ctx, id, options)
if err != nil {

3
e2e/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
videos
screenshots
__diff_output__

12
e2e/Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM cypress/included:9.0.0
RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
WORKDIR /e2e
COPY pnpm-lock.yaml ./
RUN pnpm fetch
COPY package.json ./
RUN pnpm install -r --offline

3
e2e/cypress.env.json Normal file
View File

@@ -0,0 +1,3 @@
{
"DOZZLE_DEFAULT": "http://localhost:3000/"
}

3
e2e/cypress.json Normal file
View File

@@ -0,0 +1,3 @@
{
"fixturesFolder": false
}

View File

@@ -0,0 +1,25 @@
/// <reference types="cypress" />
context("Dozzle default mode", { baseUrl: Cypress.env("DOZZLE_DEFAULT") }, () => {
beforeEach(() => {
cy.visit("/");
});
it("home screen", () => {
cy.get("li.running", { timeout: 10000 }).removeDates().matchImageSnapshot();
});
it("correct title", () => {
cy.title().should("eq", "1 containers - Dozzle");
cy.get("li.running:first a").click();
cy.title().should("include", "- Dozzle");
});
it("settings page", () => {
cy.get("a[href='/settings']").click();
cy.contains("About");
});
});

View File

@@ -0,0 +1,20 @@
/// <reference types="cypress" />
context("Dozzle settings mode", { baseUrl: Cypress.env("DOZZLE_DEFAULT") }, () => {
beforeEach(() => {
cy.visit("/version").clearLocalStorage().visit("/settings");
});
it("scrollbars", () => {
cy.contains("Use smaller scrollbars").click();
cy.get("html").should("have.class", "has-custom-scrollbars");
});
it("stopped containers", () => {
cy.contains("Show stopped containers")
.click()
.then(() => {
expect(JSON.parse(localStorage.getItem("DOZZLE_SETTINGS")).showAllContainers).to.be.true;
});
});
});

View File

@@ -0,0 +1,15 @@
/// <reference types="cypress" />
context.skip("Dozzle light mode", { baseUrl: Cypress.env("DOZZLE_DEFAULT") }, () => {
before(() => {
cy.visit("/settings");
cy.contains("Use light theme").click();
});
beforeEach(() => {
cy.visit("/");
});
it("home screen", () => {
cy.get("li.running", { timeout: 10000 }).removeDates().matchImageSnapshot();
});
});

View File

@@ -0,0 +1,7 @@
/// <reference types="cypress" />
context("Dozzle routes", { baseUrl: Cypress.env("DOZZLE_DEFAULT") }, () => {
it("show", () => {
cy.visit("/show?name=dozzle").url().should("include", "/container/");
});
});

View File

@@ -0,0 +1,26 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
const { addMatchImageSnapshotPlugin } = require("cypress-image-snapshot/plugin");
/**
* @type {Cypress.PluginConfig}
*/
// eslint-disable-next-line no-unused-vars
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
addMatchImageSnapshotPlugin(on, config);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -0,0 +1,33 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
import { addMatchImageSnapshotCommand } from "cypress-image-snapshot/command";
addMatchImageSnapshotCommand();
Cypress.Commands.add("removeDates", () => {
cy.window().then((win) => win.document.querySelectorAll("time").forEach((el) => el.remove()));
});

View File

@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "./commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@@ -8,6 +8,8 @@ services:
- DOZZLE_FILTER=name=custom_base
- DOZZLE_BASE=/foobarbase
- DOZZLE_NO_ANALYTICS=1
ports:
- "8080:8080"
build:
context: ..
dozzle:
@@ -17,18 +19,20 @@ services:
environment:
- DOZZLE_FILTER=name=dozzle
- DOZZLE_NO_ANALYTICS=1
ports:
- "9090:8080"
build:
context: ..
integration:
cypress:
build:
context: .
command: yarn test
working_dir: /e2e
volumes:
- ./__tests__:/app/__tests__
- ./cypress:/e2e/cypress
- ./cypress.json:/e2e/cypress.json
environment:
- DEFAULT_URL=http://dozzle:8080/
- CUSTOM_URL=http://custom_base:8080/foobarbase
- DOZZLE_NO_ANALYTICS=1
- CYPRESS_DOZZLE_DEFAULT=http://dozzle:8080/
- CYPRESS_CUSTOM_DEFAULT=http://custom_base:8080/foobarbase
depends_on:
- dozzle
- custom_base

10
e2e/package.json Normal file
View File

@@ -0,0 +1,10 @@
{
"name": "e2e",
"version": "1.0.0",
"scripts": {},
"license": "ISC",
"dependencies": {
"cypress": "^9.0.0",
"cypress-image-snapshot": "^4.0.1"
}
}

1447
e2e/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

12
go.mod
View File

@@ -2,27 +2,27 @@ module github.com/amir20/dozzle
require (
github.com/Microsoft/go-winio v0.5.1 // indirect
github.com/alexflint/go-arg v1.4.2
github.com/alexflint/go-arg v1.4.3
github.com/beme/abide v0.0.0-20190723115211-635a09831760
github.com/containerd/containerd v1.5.7 // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v20.10.10+incompatible
github.com/docker/docker v20.10.14+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/gogo/protobuf v1.3.2 // indirect
github.com/gorilla/mux v1.8.0
github.com/gorilla/sessions v1.2.1
github.com/magiconair/properties v1.8.5
github.com/magiconair/properties v1.8.6
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/sergi/go-diff v1.1.0 // indirect
github.com/sirupsen/logrus v1.8.1
github.com/spf13/afero v1.6.0
github.com/spf13/afero v1.8.2
github.com/stretchr/objx v0.3.0 // indirect
github.com/stretchr/testify v1.7.0
github.com/stretchr/testify v1.7.1
golang.org/x/net v0.0.0-20211104170005-ce137452f963 // indirect
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect
google.golang.org/genproto v0.0.0-20211104193956-4c6863e31247 // indirect
@@ -41,4 +41,4 @@ require (
google.golang.org/protobuf v1.27.1 // indirect
)
go 1.17
go 1.18

139
go.sum
View File

@@ -4,23 +4,38 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
@@ -66,10 +81,9 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alexflint/go-arg v1.4.2 h1:lDWZAXxpAnZUq4qwb86p/3rIJJ2Li81EoMbTMujhVa0=
github.com/alexflint/go-arg v1.4.2/go.mod h1:9iRbDxne7LcR/GSvEr7ma++GLpdIU1zrghf2y2768kM=
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-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw=
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/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
@@ -108,6 +122,7 @@ github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJ
github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
@@ -234,8 +249,8 @@ github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TT
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
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.10+incompatible h1:GKkP0T7U4ks6X3lmmHKC2QDprnpRJor2Z5a8m62R9ZM=
github.com/docker/docker v20.10.10+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v20.10.14+incompatible h1:+T9/PRYWNDo5SZl5qS1r9Mo/0Q8AwxKKPtu9S1yxM0w=
github.com/docker/docker v20.10.14+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-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
@@ -256,6 +271,7 @@ github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT
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=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
@@ -313,6 +329,8 @@ github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -336,6 +354,7 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -345,11 +364,18 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -358,6 +384,7 @@ github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
@@ -386,6 +413,7 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
@@ -423,8 +451,8 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
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.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
@@ -507,7 +535,7 @@ github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV
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=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
@@ -565,8 +593,8 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo=
github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
@@ -591,8 +619,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
@@ -617,7 +646,9 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:
github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
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=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
@@ -631,6 +662,8 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@@ -644,12 +677,13 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
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/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=
@@ -672,6 +706,7 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
@@ -680,6 +715,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
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/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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -697,6 +734,7 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -708,11 +746,18 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
@@ -723,12 +768,17 @@ golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4Iltr
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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=
@@ -776,11 +826,18 @@ golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -790,12 +847,16 @@ golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201202213521-69691e467435/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-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/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-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -848,9 +909,26 @@ golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
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/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=
@@ -866,12 +944,23 @@ google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsb
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -892,10 +981,27 @@ google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvx
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20211104193956-4c6863e31247 h1:ZONpjmFT5e+I/0/xE3XXbG5OIvX2hRYzol04MhKBl2E=
google.golang.org/genproto v0.0.0-20211104193956-4c6863e31247/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
@@ -910,9 +1016,15 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.42.0 h1:XT2/MFpuPFsEX2fWh3YQtHkZ+WYZFQRfaUgLZYj/p6A=
@@ -972,6 +1084,7 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo=
k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ=
k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8=

View File

@@ -12,6 +12,12 @@
"secured": "{{ .Secured }}"
}
</script>
<link
rel="icon"
href="data:image/svg+xml,%3Csvg%20width%3D%22128%22%20height%3D%22128%22%20viewBox%3D%220%200%20128%20128%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Crect%20width%3D%22128%22%20height%3D%22128%22%20rx%3D%226%22%20fill%3D%22%23222222%22%2F%3E%0A%3Cpath%20d%3D%22M82.3248%2094.3863H123V104.093H67.8025V95.3506L106.164%2044.3736H68.3808V34.5382H121.072V42.9594L82.3248%2094.3863Z%22%20fill%3D%22%23FFDD57%22%2F%3E%0A%3Cpath%20d%3D%22M8%20107.107L17.5656%2014L43.8372%2016.7013C51.9339%2017.5338%2058.9091%2020.0604%2064.7629%2024.2812C70.6166%2028.5019%2074.8873%2034.0893%2077.5749%2041.0432C80.3052%2048.0016%2081.2514%2055.7674%2080.4137%2064.3407L79.8027%2070.2877C78.9005%2079.0698%2076.4053%2086.5894%2072.3173%2092.8468C68.2719%2099.1084%2062.914%20103.684%2056.2436%20106.574C49.6158%20109.468%2042.1213%20110.529%2033.7602%20109.755L8%20107.107ZM28.8005%2025.3655L21.3043%2098.3288L34.2164%2099.6565C43.6767%20100.629%2051.3299%2098.4435%2057.1758%2093.0993C63.0644%2087.7595%2066.5671%2079.6542%2067.684%2068.7832L68.2424%2063.3477C69.3286%2052.7752%2067.6788%2044.3123%2063.293%2037.9592C58.9542%2031.5678%2052.2295%2027.8607%2043.1188%2026.8377L28.8005%2025.3655Z%22%20fill%3D%22%23FFDD57%22%2F%3E%0A%3C%2Fsvg%3E%0A"
type="image/svg+xml"
/>
<script type="module" src="/assets/main.ts"></script>
</head>
<body>
<svg

View File

@@ -1 +0,0 @@
node_modules

View File

@@ -1 +0,0 @@
__diff_output__

View File

@@ -1,8 +0,0 @@
FROM amir20/docker-alpine-puppeteer:v1
COPY package*.json yarn.lock /app/
RUN yarn
COPY . /app/
CMD ["yarn", "test"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -1,22 +0,0 @@
const { removeTimes } = require("../utils");
const { CUSTOM_URL: URL } = process.env;
describe("Dozzle with custom base", () => {
beforeEach(async () => {
await page.goto(URL, { waitUntil: "networkidle2" });
});
it("renders full page on desktop", async () => {
await removeTimes(page);
const image = await page.screenshot({ fullPage: true });
expect(image).toMatchImageSnapshot();
});
it("and shows one container with correct title", async () => {
await removeTimes(page);
const menuTitle = await page.$eval("aside ul.menu-list li a", (e) => e.title);
expect(menuTitle).toEqual("custom_base");
});
});

View File

@@ -1,76 +0,0 @@
const puppeteer = require("puppeteer");
const { removeTimes } = require("../utils");
const iPhoneX = puppeteer.devices["iPhone X"];
const iPadLandscape = puppeteer.devices["iPad landscape"];
const { DEFAULT_URL: URL } = process.env;
describe("home page", () => {
beforeEach(async () => {
await page.goto(URL, { waitUntil: "networkidle2" });
});
it("renders full page on desktop", async () => {
await removeTimes(page);
const image = await page.screenshot({ fullPage: true });
expect(image).toMatchImageSnapshot();
});
it("renders ipad viewport", async () => {
await page.emulate(iPadLandscape);
await removeTimes(page);
const image = await page.screenshot();
expect(image).toMatchImageSnapshot();
});
it("renders iphone viewport", async () => {
await page.emulate(iPhoneX);
await removeTimes(page);
const image = await page.screenshot();
expect(image).toMatchImageSnapshot();
});
it("displays iphone menu", async () => {
await page.emulate(iPhoneX);
await page.click("a.navbar-burger");
const menuText = await page.$eval("aside ul.menu-list.is-hidden-mobile li a", (e) => e.textContent);
expect(menuText.trim()).toEqual("dozzle");
});
describe("has menu visible", () => {
beforeAll(async () => {
await jestPuppeteer.resetBrowser();
});
beforeEach(async () => {
await page.goto(URL, { waitUntil: "networkidle2" });
});
it("and shows one container with correct title", async () => {
const menuTitle = await page.$eval("aside ul.menu-list li a", (e) => e.title);
expect(menuTitle).toEqual("dozzle");
});
it("and menu is clickable", async () => {
await page.click("aside ul.menu-list li a");
const className = await page.$eval("aside ul.menu-list li a", (e) => e.className);
expect(className).toContain("router-link-exact-active");
});
it("and when clicked shows logs", async () => {
await page.click("aside ul.menu-list li a");
await page.waitForSelector("ul.events li span.text");
const text = await page.$eval("ul.events li:nth-child(1) span.text", (e) => e.textContent);
expect(text).toContain("Dozzle version dev");
});
});
});

View File

@@ -1,41 +0,0 @@
const puppeteer = require("puppeteer");
const { removeTimes } = require("../utils");
const iPhoneX = puppeteer.devices["iPhone X"];
const iPadLandscape = puppeteer.devices["iPad landscape"];
const { DEFAULT_URL: URL } = process.env;
describe("Dozzle with light mode", () => {
beforeAll(async () => {
await page.goto(URL + "/settings", { waitUntil: "networkidle2" });
await page.$$eval("label.switch", (elements) => {
elements.filter((e) => e.textContent.trim() === "Use light theme")[0].click();
});
});
beforeEach(async () => {
await page.goto(URL, { waitUntil: "networkidle2" });
});
it("renders full page on desktop", async () => {
await removeTimes(page);
const image = await page.screenshot({ fullPage: true });
expect(image).toMatchImageSnapshot();
});
it("renders ipad viewport", async () => {
await page.emulate(iPadLandscape);
await removeTimes(page);
const image = await page.screenshot();
expect(image).toMatchImageSnapshot();
});
it("renders iphone viewport", async () => {
await page.emulate(iPhoneX);
await removeTimes(page);
const image = await page.screenshot();
expect(image).toMatchImageSnapshot();
});
});

View File

@@ -1,9 +0,0 @@
module.exports = {
launch: {
headless: process.env.HEADLESS !== "false",
defaultViewport: { width: 1920, height: 1200 },
args: ["--no-sandbox", "--disable-setuid-sandbox"],
executablePath: process.env.CHROME_EXE_PATH || "",
},
browserContext: "incognito",
};

View File

@@ -1,5 +0,0 @@
const { toMatchImageSnapshot } = require("jest-image-snapshot");
expect.extend({ toMatchImageSnapshot });
jest.setTimeout(5000);

View File

@@ -1,24 +0,0 @@
{
"name": "test",
"version": "1.0.0",
"description": "",
"scripts": {
"test": "jest"
},
"author": "",
"license": "ISC",
"dependencies": {
"jest": "^27.0.6",
"jest-image-snapshot": "^4.0.0",
"puppeteer": "^11.0.0"
},
"jest": {
"preset": "jest-puppeteer",
"setupFilesAfterEnv": [
"<rootDir>/jest-setup.js"
]
},
"devDependencies": {
"jest-puppeteer": "^6.0.0"
}
}

View File

@@ -1,8 +0,0 @@
async function removeTimes(page) {
await page.waitForSelector("time");
await page.evaluate(() => {
(document.querySelectorAll("time") || []).forEach((el) => el.remove());
});
}
module.exports = { removeTimes };

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +0,0 @@
module.exports = {
clearMocks: true,
testEnvironment: "jsdom",
moduleFileExtensions: ["js", "json", "vue"],
coveragePathIgnorePatterns: ["node_modules"],
testPathIgnorePatterns: ["node_modules", "<rootDir>/integration/"],
transformIgnorePatterns: ["node_modules"],
watchPathIgnorePatterns: ["<rootDir>/node_modules/"],
snapshotSerializers: ["jest-serializer-vue"],
transform: {
".*\\.vue$": "vue-jest",
"^.+\\.js$": "babel-jest",
},
};

15
jest.config.ts Normal file
View File

@@ -0,0 +1,15 @@
import type { Config } from "@jest/types";
const config: Config.InitialOptions = {
preset: "ts-jest",
testEnvironment: "jsdom",
testPathIgnorePatterns: ["node_modules", "<rootDir>/integration/", "<rootDir>/e2e/"],
transform: {
"^.+\\.vue$": "@vue/vue3-jest",
},
moduleNameMapper: {
"@/(.*)": ["<rootDir>/assets/$1"],
},
};
export default config;

View File

@@ -1,3 +0,0 @@
{
"include": ["./assets/**/*"]
}

49
main.go
View File

@@ -25,27 +25,29 @@ var (
)
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."`
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."`
Key string `arg:"env:DOZZLE_KEY" help:"set a random key for username and password. This is required for auth."`
Username string `arg:"env:DOZZLE_USERNAME" help:"sets the username for auth."`
Password string `arg:"env:DOZZLE_PASSWORD" help:"sets password for auth"`
NoAnalytics bool `arg:"--no-analytics,env:DOZZLE_NO_ANALYTICS" help:"disables anonymous analytics"`
FilterStrings []string `arg:"env:DOZZLE_FILTER,--filter,separate" help:"filters docker containers using Docker syntax."`
Filter map[string][]string `arg:"-"`
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."`
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."`
Key string `arg:"env:DOZZLE_KEY" help:"set a random key for username and password. This is required for auth."`
Username string `arg:"env:DOZZLE_USERNAME" help:"sets the username for auth."`
Password string `arg:"env:DOZZLE_PASSWORD" help:"sets password for auth"`
NoAnalytics bool `arg:"--no-analytics,env:DOZZLE_NO_ANALYTICS" help:"disables anonymous analytics"`
WaitForDockerSeconds int `arg:"--wait-for-docker-seconds,env:DOZZLE_WAIT_FOR_DOCKER_SECONDS" help:"wait for docker to be available for at most this many seconds before starting the server."`
FilterStrings []string `arg:"env:DOZZLE_FILTER,--filter,separate" help:"filters docker containers using Docker syntax."`
Filter map[string][]string `arg:"-"`
}
func (args) Version() string {
return version
}
//go:embed static
//go:embed dist
var content embed.FS
func main() {
var args args
var err error
parser := arg.MustParse(&args)
args.Filter = make(map[string][]string)
@@ -69,10 +71,17 @@ func main() {
log.Infof("Dozzle version %s", version)
dockerClient := docker.NewClientWithFilters(args.Filter)
_, err := dockerClient.ListContainers()
if err != nil {
log.Fatalf("Could not connect to Docker Engine: %v", err)
for i := 1; ; i++ {
_, err := dockerClient.ListContainers()
if err == nil {
break
} else if args.WaitForDockerSeconds <= 0 {
log.Fatalf("Could not connect to Docker Engine: %v", err)
} else {
log.Infof("Waiting for Docker Engine (attempt %d): %s", i, err)
time.Sleep(5 * time.Second)
args.WaitForDockerSeconds -= 5
}
}
if args.Username != "" || args.Password != "" {
@@ -95,17 +104,17 @@ func main() {
Password: args.Password,
}
static, err := fs.Sub(content, "static")
assets, err := fs.Sub(content, "dist")
if err != nil {
log.Fatalf("Could not open embedded static folder: %v", err)
log.Fatalf("Could not open embedded dist folder: %v", err)
}
if _, ok := os.LookupEnv("LIVE_FS"); ok {
log.Info("Using live filesystem at ./static")
static = os.DirFS("./static")
log.Info("Using live filesystem at ./dist")
assets = os.DirFS("./dist")
}
srv := web.CreateServer(dockerClient, static, config)
srv := web.CreateServer(dockerClient, assets, config)
go doStartEvent(args)
go func() {
log.Infof("Accepting connections on %s", srv.Addr)

View File

@@ -1,6 +1,6 @@
{
"name": "dozzle",
"version": "3.9.0",
"version": "3.12.0",
"description": "Realtime log viewer for docker containers. ",
"homepage": "https://github.com/amir20/dozzle#readme",
"bugs": {
@@ -11,74 +11,63 @@
"url": "git+https://github.com/amir20/dozzle.git"
},
"license": "ISC",
"author": "",
"author": "Amir Raminfar <findamir@gmail.com>",
"scripts": {
"watch": "npm-run-all -p watch:*",
"watch:assets": "webpack --mode=development --watch",
"watch:server": "LIVE_FS=true reflex -c .reflex",
"dev": "make fake_static && npm-run-all -p dev-server watch:server",
"dev-server": "webpack serve --mode=development",
"build": "rm -rf static && webpack --mode=production --progress",
"clean": "rm -rf static",
"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 jest",
"test": "TZ=UTC vitest",
"postinstall": "husky install"
},
"dependencies": {
"@babel/core": "^7.15.8",
"@babel/plugin-transform-runtime": "^7.15.8",
"@vue/component-compiler-utils": "^3.3.0",
"@vue/test-utils": "^1.2.2",
"@iconify-json/carbon": "^1.1.3",
"@iconify-json/cil": "^1.1.1",
"@iconify-json/mdi": "^1.1.9",
"@iconify-json/mdi-light": "^1.1.1",
"@iconify-json/octicon": "^1.1.6",
"@oruga-ui/oruga-next": "^0.5.4",
"@oruga-ui/theme-bulma": "^0.2.5",
"@vitejs/plugin-vue": "^2.3.1",
"@vue/compiler-sfc": "^3.2.33",
"@vueuse/core": "^8.2.6",
"ansi-to-html": "^0.7.2",
"autoprefixer": "^10.4.0",
"babel-core": "^7.0.0-bridge.0",
"babel-jest": "^27.3.1",
"babel-preset-env": "^1.7.0",
"buefy": "^0.9.10",
"bulma": "^0.9.3",
"caniuse-lite": "^1.0.30001272",
"css-loader": "^6.5.0",
"date-fns": "^2.25.0",
"dompurify": "^2.3.3",
"eventsourcemock": "^2.0.0",
"fuzzysort": "^1.1.4",
"hotkeys-js": "^3.8.7",
"html-webpack-plugin": "^5.5.0",
"date-fns": "^2.28.0",
"fuzzysort": "^1.2.1",
"hotkeys-js": "^3.8.9",
"lodash.debounce": "^4.0.8",
"lodash.throttle": "^4.1.1",
"mini-css-extract-plugin": "^2.4.3",
"postcss": "^8.3.11",
"postcss-loader": "^6.2.0",
"sass": "^1.43.4",
"sass-loader": "^12.3.0",
"semver": "^7.3.5",
"splitpanes": "^2.3.8",
"store": "^2.0.12",
"unplugin-icons": "^0.12.18",
"vue": "^2.6.14",
"vue-loader": "^15.9.8",
"vue-meta": "^2.4.0",
"vue-router": "^3.5.3",
"vue-style-loader": "^4.1.3",
"vue-template-compiler": "^2.6.14",
"vuex": "^3.6.2",
"webpack": "^5.61.0",
"webpack-cli": "^4.9.1",
"webpack-pwa-manifest": "^4.3.0"
"pinia": "^2.0.13",
"sass": "^1.50.0",
"semver": "^7.3.7",
"splitpanes": "^3.1.1",
"typescript": "^4.6.2",
"unplugin-auto-import": "^0.7.1",
"unplugin-icons": "^0.14.1",
"unplugin-vue-components": "^0.19.3",
"vite": "^2.9.5",
"vue": "^3.2.33",
"vue-router": "^4.0.13"
},
"devDependencies": {
"@iconify-json/mdi-light": "^1.0.1",
"@iconify-json/octicon": "^1.0.5",
"@pinia/testing": "^0.0.11",
"@types/jest": "^27.4.1",
"@types/lodash.debounce": "^4.0.6",
"@types/node": "^17.0.24",
"@types/semver": "^7.3.9",
"@vue/test-utils": "^2.0.0-rc.20",
"c8": "^7.11.0",
"eventsourcemock": "^2.0.0",
"husky": "^7.0.4",
"jest": "^27.3.1",
"jest-serializer-vue": "^2.0.2",
"lint-staged": "^11.2.6",
"jsdom": "^19.0.0",
"lint-staged": "^12.3.8",
"npm-run-all": "^4.1.5",
"prettier": "^2.4.1",
"release-it": "^14.11.6",
"vue-hot-reload-api": "^2.3.4",
"vue-jest": "^3.0.7",
"webpack-dev-server": "^4.4.0"
"prettier": "^2.6.2",
"release-it": "^14.14.2",
"ts-node": "^10.6.0",
"vitest": "^0.9.3"
},
"lint-staged": {
"*.{js,vue,css}": [

6590
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

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