Compare commits

...

255 Commits

Author SHA1 Message Date
Amir Raminfar
37dba2495e Release 3.12.6 2022-05-17 09:41:00 -07:00
Amir Raminfar
32b1d62773 Makes the padding a little tighter 2022-05-16 15:53:22 -07:00
Amir Raminfar
b366a85248 Release 3.12.5 2022-05-16 09:52:30 -07:00
Amir Raminfar
57008b9c94 Adds a loaders for left menu (#1750)
* Adds a loader

* Adds a loader and cleans up home page

* Fixes missing var
2022-05-16 09:45:54 -07:00
Amir Raminfar
8448b4ffa0 Adds alternate coloring 2022-05-13 09:38:03 -07:00
kodiakhq[bot]
612e74faff Merge pull request #1748 from amir20/dependabot/go_modules/github.com/docker/docker-20.10.16incompatible
Bump github.com/docker/docker from 20.10.15+incompatible to 20.10.16+incompatible
2022-05-13 09:15:01 +00:00
dependabot[bot]
afc225520b Bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 20.10.15+incompatible to 20.10.16+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.15...v20.10.16)

---
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-05-13 09:10:45 +00:00
Amir Raminfar
a437e52dac Updates modules 2022-05-12 10:29:37 -07:00
kodiakhq[bot]
584d027e58 Merge pull request #1747 from amir20/dependabot/docker/golang-1.18.2-alpine
Bump golang from 1.18.1-alpine to 1.18.2-alpine
2022-05-11 09:23:15 +00:00
dependabot[bot]
5877ca9a0a Bump golang from 1.18.1-alpine to 1.18.2-alpine
Bumps golang from 1.18.1-alpine to 1.18.2-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-05-11 09:17:28 +00:00
kodiakhq[bot]
d176be7654 Merge pull request #1745 from amir20/dependabot/docker/e2e/cypress/included-9.6.1
Bump cypress/included from 9.6.0 to 9.6.1 in /e2e
2022-05-10 10:00:16 +00:00
dependabot[bot]
69d1534204 Bump cypress/included from 9.6.0 to 9.6.1 in /e2e
Bumps cypress/included from 9.6.0 to 9.6.1.

---
updated-dependencies:
- dependency-name: cypress/included
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-10 09:56:01 +00:00
Amir Raminfar
ee30bb3821 Updates node and pnpm 2022-05-06 09:22:47 -07:00
kodiakhq[bot]
4a45204a4b Merge pull request #1740 from amir20/dependabot/go_modules/github.com/docker/docker-20.10.15incompatible
Bump github.com/docker/docker from 20.10.14+incompatible to 20.10.15+incompatible
2022-05-06 09:21:23 +00:00
kodiakhq[bot]
03f673c647 Merge pull request #1739 from amir20/dependabot/github_actions/docker/setup-qemu-action-2.0.0
Bump docker/setup-qemu-action from 1.2.0 to 2.0.0
2022-05-06 09:21:15 +00:00
dependabot[bot]
7c846e40cf Bump docker/setup-qemu-action from 1.2.0 to 2.0.0
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 1.2.0 to 2.0.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v1.2.0...v2.0.0)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-06 09:16:54 +00:00
kodiakhq[bot]
405980862c Merge pull request #1738 from amir20/dependabot/github_actions/docker/login-action-2.0.0
Bump docker/login-action from 1.14.1 to 2.0.0
2022-05-06 09:16:31 +00:00
kodiakhq[bot]
0f3ab6f0c0 Merge pull request #1737 from amir20/dependabot/github_actions/docker/metadata-action-4
Bump docker/metadata-action from 3 to 4
2022-05-06 09:16:16 +00:00
kodiakhq[bot]
e12890510f Merge pull request #1736 from amir20/dependabot/github_actions/docker/build-push-action-3.0.0
Bump docker/build-push-action from 2.10.0 to 3.0.0
2022-05-06 09:16:06 +00:00
kodiakhq[bot]
b0701da4bf Merge pull request #1735 from amir20/dependabot/github_actions/docker/setup-buildx-action-2.0.0
Bump docker/setup-buildx-action from 1.7.0 to 2.0.0
2022-05-06 09:15:53 +00:00
dependabot[bot]
048195e0e6 Bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 20.10.14+incompatible to 20.10.15+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.14...v20.10.15)

---
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-05-06 09:11:06 +00:00
dependabot[bot]
81425f1f06 Bump docker/login-action from 1.14.1 to 2.0.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 1.14.1 to 2.0.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v1.14.1...v2.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-06 09:10:53 +00:00
dependabot[bot]
af02ea27a0 Bump docker/metadata-action from 3 to 4
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 3 to 4.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Upgrade guide](https://github.com/docker/metadata-action/blob/master/UPGRADE.md)
- [Commits](https://github.com/docker/metadata-action/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-06 09:10:51 +00:00
dependabot[bot]
2800dddbf6 Bump docker/build-push-action from 2.10.0 to 3.0.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 2.10.0 to 3.0.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v2.10.0...v3.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-06 09:10:48 +00:00
dependabot[bot]
43cd2c64ab Bump docker/setup-buildx-action from 1.7.0 to 2.0.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 1.7.0 to 2.0.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v1.7.0...v2.0.0)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-06 09:10:45 +00:00
Amir Raminfar
5854eace7b Release 3.12.4 2022-05-02 10:25:07 -07:00
Amir Raminfar
399c0b0ff4 Fixes releases for github 2022-05-02 10:24:56 -07:00
Amir Raminfar
f49a778035 Release 3.12.3 2022-05-02 09:50:45 -07:00
Amir Raminfar
395549aec9 Fixes build 2022-05-02 09:27:50 -07:00
Amir Raminfar
94dd6067ca Fixes #1732 2022-05-02 08:55:28 -07:00
kodiakhq[bot]
aee5734e74 Merge pull request #1726 from amir20/dependabot/docker/node-18-alpine
Bump node from 17-alpine to 18-alpine
2022-04-29 16:07:31 +00:00
kodiakhq[bot]
235db9dae5 Merge pull request #1729 from amir20/dependabot/github_actions/docker/setup-buildx-action-1.7.0
Bump docker/setup-buildx-action from 1.6.0 to 1.7.0
2022-04-29 16:07:21 +00:00
kodiakhq[bot]
84d22248a4 Merge pull request #1730 from amir20/dependabot/docker/e2e/cypress/included-9.6.0
Bump cypress/included from 9.5.4 to 9.6.0 in /e2e
2022-04-29 16:07:10 +00:00
dependabot[bot]
207468d0f0 Bump node from 17-alpine to 18-alpine
Bumps node from 17-alpine to 18-alpine.

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-29 16:02:54 +00:00
dependabot[bot]
1409e45f8d Bump docker/setup-buildx-action from 1.6.0 to 1.7.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 1.6.0 to 1.7.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v1.6.0...v1.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-29 16:02:43 +00:00
dependabot[bot]
e8306d67b6 Bump cypress/included from 9.5.4 to 9.6.0 in /e2e
Bumps cypress/included from 9.5.4 to 9.6.0.

---
updated-dependencies:
- dependency-name: cypress/included
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-29 16:02:36 +00:00
Amir Raminfar
d60614ada7 Updates commit message to multiline 2022-04-29 08:56:39 -07:00
Amir Raminfar
a287d7b2b4 Updates depedabot 2022-04-28 11:57:27 -07:00
Amir Raminfar
48d34f3f58 Updates npm modules 2022-04-26 14:16:43 -07:00
Amir Raminfar
098924a8f9 Release 3.12.2 2022-04-21 16:25:03 -07:00
Amir Raminfar
2d2ff05987 Removes dead containers correctly 2022-04-21 16:20:58 -07:00
Amir Raminfar
32db78d64d Release 3.12.1 2022-04-21 10:38:16 -07:00
Amir Raminfar
ff4f7126f9 Fixes push 2022-04-21 10:33:20 -07:00
Amir Raminfar
590d3bd4f8 Removes echo debugging 2022-04-21 10:31:10 -07:00
Amir Raminfar
abf0507307 Adds log message using new syntax (#1724)
* Adds log message using new syntax
2022-04-21 10:27:51 -07:00
Amir Raminfar
89e5bee174 Updates cypress (#1723) 2022-04-20 16:32:17 -07:00
Amir Raminfar
7fa8bec6b8 Reverts message lable 2022-04-20 16:21:55 -07:00
Amir Raminfar
6459e84b80 Tries fixing with simpler solution 2022-04-20 16:17:45 -07:00
Amir Raminfar
e16470affd Adds missing " 2022-04-20 16:15:58 -07:00
Amir Raminfar
8f812b633b Tries again to pass COMMIT_MESSAGE 2022-04-20 16:13:41 -07:00
Amir Raminfar
08eaf8d898 Tries to pass message 2022-04-20 16:07:13 -07:00
Amir Raminfar
b9bc7af1d6 Removes overflow hidden to fix scroll bug. See #1720 (#1721) 2022-04-20 15:59:10 -07:00
Amir Raminfar
b75974e850 Adds sha 2022-04-20 15:58:57 -07:00
Amir Raminfar
b1fa9ea672 Tries to fix int tests 2022-04-20 15:47:10 -07:00
Amir Raminfar
0cac350493 Records tests with cypress 2022-04-20 15:25:05 -07:00
Amir Raminfar
b8ed2db0f0 Fixes int test 2022-04-20 14:48:36 -07:00
Amir Raminfar
5d9db17b9c Adds total CPU and Mem usage. See #1715 2022-04-20 12:46:21 -07:00
Amir Raminfar
038e2dee88 Removes name from menus 2022-04-19 15:12:08 -07:00
Amir Raminfar
5b15fc2972 Updates style for date 2022-04-19 14:56:14 -07:00
Amir Raminfar
4035e2e262 Fixes deault settings 2022-04-19 14:46:29 -07:00
Amir Raminfar
011bc94e8c Cleans up search by removing for loop (#1719)
* Cleans up search by removing for loop

* Fixes tests
2022-04-19 13:54:08 -07:00
Amir Raminfar
49448790ff Adds soft wraps as an option and fixes #1696 (#1718) 2022-04-18 11:44:23 -07:00
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
Amir Raminfar
b72e208f27 Release 3.9.0 2021-11-09 14:15:43 -08:00
Amir Raminfar
0714809fd9 Renames job 2021-11-09 14:07:50 -08:00
Amir Raminfar
17d43453cc Renames dev job 2021-11-09 14:05:07 -08:00
Amir Raminfar
ce120ac194 Adds pr pushes 2021-11-08 11:38:06 -08:00
Amir Raminfar
f19bbb8d38 Fixes int tests 2021-11-07 14:42:55 -08:00
Amir Raminfar
4f7cbb7cdf Fixes test cases 2021-11-07 14:39:27 -08:00
Coteh
3672a4729d Use .icon class instead of vertically aligning through CSS 2021-11-07 15:46:32 -05:00
Coteh
b0d1cd257c Add autoInstall setting for unplugin icons 2021-11-07 15:35:54 -05:00
Coteh
be23ef93eb Update pnpm-lock.yaml 2021-11-07 15:02:27 -05:00
Coteh
07d3176178 Switch to unplugin-icons for icons
The following icons are used:
Material Design Light
- Chevron Left (Hide Sidebar)
- Chevron Right (Show Sidebar)
- Chevron Double Down (Page Down)
- Magnify (Search)
- Cog (Settings)

Octicon
- Trash 24 (Clear Logs)
- Download 24 (Download Logs)
- Container 24 (Container Listing)

CoreUI Free
- Columns (Pin as Column)
2021-11-07 15:02:27 -05:00
Amir Raminfar
b01020dc0e Removes yarn 2021-11-07 07:20:56 -08:00
Amir Raminfar
4e5fedb18f Fixes pnpm 2021-11-07 07:03:46 -08:00
Amir Raminfar
dcd1fcfcde Updates gh actions 2021-11-07 07:02:09 -08:00
Amir Raminfar
fb777d4dbf Fixes yarn 2021-11-05 19:09:39 -07:00
Amir Raminfar
7b1f4f7f34 Remoges unused import 2021-11-05 19:09:39 -07:00
kodiakhq[bot]
d88eb339b4 Merge pull request #1591 from amir20/dependabot/docker/golang-1.17.3-alpine
Bump golang from 1.17.2-alpine to 1.17.3-alpine
2021-11-05 09:16:20 +00:00
dependabot[bot]
a84ef7be66 Bump golang from 1.17.2-alpine to 1.17.3-alpine
Bumps golang from 1.17.2-alpine to 1.17.3-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-11-05 09:11:37 +00:00
Amir Raminfar
fc798985fd Fixes follow ups #1584 2021-11-04 19:03:33 -07:00
Amir Raminfar
df176c39f5 Updates go modules 2021-11-04 18:54:42 -07:00
James Cote
49b39fb3af Add clear logs functionality (#1584)
* Add clear logs functionality

Other Changes:
- Add actions toolbar for actions pertaining to container logs
- Move Download button to actions toolbar, remove text and margin to match clear logs button
- Move search box below actions toolbar
- Turn off pointer events for scroll progress component so that Download button can be clicked
- Adjust spacing of container bar to accomodate the new actions toolbar

* Change actions toolbar into a floating menu

Other Changes:
- Move actions toolbar into its own component
- Move clear hotkey from LogEventSource to LogActionsToolbar, where the button is
- Move search back to where it was before
- Add tooltip component from buefy to annotate action buttons
- Add CSS variable for action toolbar background color, changes based on theme
2021-11-04 18:48:51 -07:00
Amir Raminfar
d9e8cca867 Fixes #1589 2021-11-04 15:52:53 -07:00
kodiakhq[bot]
bdead5c55d Merge pull request #1590 from amir20/dependabot/npm_and_yarn/integration/puppeteer-11.0.0
Bump puppeteer from 10.4.0 to 11.0.0 in /integration
2021-11-04 09:08:44 +00:00
dependabot[bot]
05b0525a4b Bump puppeteer from 10.4.0 to 11.0.0 in /integration
Bumps [puppeteer](https://github.com/puppeteer/puppeteer) from 10.4.0 to 11.0.0.
- [Release notes](https://github.com/puppeteer/puppeteer/releases)
- [Changelog](https://github.com/puppeteer/puppeteer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/puppeteer/puppeteer/compare/v10.4.0...v11.0.0)

---
updated-dependencies:
- dependency-name: puppeteer
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-04 09:04:10 +00:00
kodiakhq[bot]
fa502cdda3 Merge pull request #1588 from amir20/dependabot/github_actions/actions/checkout-2.4.0
Bump actions/checkout from 2.3.5 to 2.4.0
2021-11-03 09:12:58 +00:00
dependabot[bot]
dee345b618 Bump actions/checkout from 2.3.5 to 2.4.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 2.3.5 to 2.4.0.
- [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.3.5...v2.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-03 09:08:25 +00:00
Amir Raminfar
d55f78829e Moves stats to narrow and possibly improves #1582 2021-11-01 14:42:00 -07:00
Amir Raminfar
8f4264e26a Update README.md 2021-10-29 15:07:03 -07:00
Amir Raminfar
c79ce7237e Release 3.8.5 2021-10-29 14:47:56 -07:00
Amir Raminfar
eeec34b018 Removes @sha... 2021-10-29 14:26:50 -07:00
Amir Raminfar
69acb24aee Fixes prod deps 2021-10-29 14:26:27 -07:00
kodiakhq[bot]
61afc74215 Merge pull request #1580 from amir20/dependabot/docker/node-17-alpine
Bump node from 16-alpine to 17-alpine
2021-10-29 18:13:43 +00:00
dependabot[bot]
396f4be965 Bump node from 16-alpine to 17-alpine
Bumps node from 16-alpine to 17-alpine.

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-29 18:09:33 +00:00
111 changed files with 5883 additions and 11455 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,10 +31,17 @@ updates:
schedule:
interval: daily
- package-ecosystem: npm
directory: "/integration"
directory: "/e2e"
labels:
- "npm"
- "dependencies"
- "automerge"
schedule:
interval: daily
- package-ecosystem: "docker"
directory: "/e2e"
labels:
- "dependencies"
- "automerge"
schedule:
interval: "daily"

View File

@@ -9,23 +9,27 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2.3.5
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.2.1
with:
version: 6.20.1
- name: Install dependencies
run: yarn
run: pnpm install
- name: Run Tests
run: yarn test
run: pnpm run test
go-test:
name: Go Tests
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.3.5
uses: actions/checkout@v3
- name: Run Go Tests with Coverage
run: make test SKIP_ASSET=1
int-test:
@@ -33,11 +37,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2.3.5
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
@@ -45,20 +49,20 @@ jobs:
steps:
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
uses: docker/metadata-action@v4
with:
images: amir20/dozzle
- name: Set up QEMU
uses: docker/setup-qemu-action@v1.2.0
uses: docker/setup-qemu-action@v2.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1.6.0
uses: docker/setup-buildx-action@v2.0.0
- name: Login to DockerHub
uses: docker/login-action@v1.10.0
uses: docker/login-action@v2.0.0
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@v3.0.0
with:
push: true
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
@@ -73,14 +77,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2.3.5
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.2.1
with:
version: 6.20.1
- name: Install dependencies
run: yarn
run: pnpm install
- name: Release to Github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: yarn release --github.release --no-increment --no-git --ci
run: pnpm run release -- --github.release --no-increment --no-git --ci

View File

@@ -2,28 +2,32 @@ on:
push:
branches:
- master
name: Push master container
pull_request:
branches:
- master
name: Push container
jobs:
buildx:
name: Push master
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
uses: docker/metadata-action@v3
uses: docker/metadata-action@v4
with:
images: amir20/dozzle
- name: Set up QEMU
uses: docker/setup-qemu-action@v1.2.0
uses: docker/setup-qemu-action@v2.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1.6.0
uses: docker/setup-buildx-action@v2.0.0
- name: Login to DockerHub
uses: docker/login-action@v1.10.0
uses: docker/login-action@v2.0.0
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@v3.0.0
with:
push: true
platforms: linux/amd64

View File

@@ -1,4 +1,10 @@
on: push
on:
push:
branches:
- master
pull_request:
branches:
- master
name: Test
jobs:
npm-test:
@@ -6,23 +12,27 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2.3.5
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.2.1
with:
version: 6.20.1
- name: Install dependencies
run: yarn
run: pnpm install
- name: Run Tests
run: yarn test
run: pnpm run test
go-test:
name: Go Tests
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.3.5
uses: actions/checkout@v3
- name: Run Go Tests with Coverage
run: make test SKIP_ASSET=1
int-test:
@@ -30,8 +40,22 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2.3.5
uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Build images
run: docker-compose -f integration/docker-compose.test.yml build
run: docker-compose -f e2e/docker-compose.yml build
- name: Set commit message for push
if: github.event_name == 'push'
run: |
echo "GIT_LOG_MESSAGE<<EOF" >> $GITHUB_ENV
git log -1 --pretty=%B ${GITHUB_SHA} >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Set commit message for pull request
if: github.event_name == 'pull_request'
run: |
echo "GIT_LOG_MESSAGE<<EOF" >> $GITHUB_ENV
git log -1 --pretty=%B ${{github.event.pull_request.head.sha}} >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Run tests
run: 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

@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname $0)/_/husky.sh"
yarn lint-staged
pnpm lint-staged

View File

@@ -1,27 +1,25 @@
# Build assets
FROM node:16-alpine as node
FROM node:18-alpine as node
RUN apk add --no-cache git openssh make g++ util-linux python3 && npm install -g pnpm
RUN apk add --no-cache git openssh make g++ util-linux curl python3 && curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
WORKDIR /build
# Install dependencies from lock file
COPY pnpm-lock.yaml ./
RUN pnpm fetch
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
# Install dependencies
RUN pnpm install -r --offline
RUN pnpm install -r --offline --prod --ignore-scripts && pnpm build
# Do the build
RUN pnpm build
FROM golang:1.17.2-alpine AS builder
FROM golang:1.18.2-alpine AS builder
RUN apk add --no-cache git ca-certificates && mkdir /dozzle
@@ -32,10 +30,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
@@ -45,7 +46,7 @@ RUN CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=$TAG" -o dozzle
FROM scratch
ENV PATH=/bin
ENV PATH /bin
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=builder /dozzle/dozzle /dozzle

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

@@ -6,8 +6,7 @@ Dozzle is a small lightweight application with a web based interface to monitor
[![Go Report Card](https://goreportcard.com/badge/github.com/amir20/dozzle)](https://goreportcard.com/report/github.com/amir20/dozzle)
[![Docker Pulls](https://img.shields.io/docker/pulls/amir20/dozzle.svg)](https://hub.docker.com/r/amir20/dozzle/)
[![Docker Size](https://images.microbadger.com/badges/image/amir20/dozzle.svg)](https://hub.docker.com/r/amir20/dozzle/)
[![Docker Version](https://images.microbadger.com/badges/version/amir20/dozzle.svg)](https://hub.docker.com/r/amir20/dozzle/)
[![Docker Version](https://img.shields.io/docker/v/amir20/dozzle?sort=semver)](https://hub.docker.com/r/amir20/dozzle/)
![Test](https://github.com/amir20/dozzle/workflows/Test/badge.svg)
## Features
@@ -169,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,54 +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: "" }));
const localVue = createLocalVue();
localVue.use(Vuex);
describe("<App />", () => {
const stubs = { RouterLink: RouterLinkStub, "router-view": true, 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,122 +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">
<icon :name="collapseNav ? 'chevron-right' : 'chevron-left'"></icon>
<span class="icon ml-2" v-if="collapseNav">
<mdi-light-chevron-right />
</span>
<span class="icon" v-else>
<mdi-light-chevron-left />
</span>
</button>
</main>
</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 Icon from "./components/Icon";
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";
export default {
name: "App",
components: {
Icon,
SideMenu,
LogContainer,
MobileMenu,
Splitpanes,
PastTime,
Pane,
},
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 collapseNav = ref(false);
const { oruga } = useProgrammatic();
const { authorizationNeeded } = config;
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,52 +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"
>
<icon-stub
name="chevron-left"
/>
</span>
</button>
</main>
`;

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

@@ -0,0 +1,42 @@
// 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 {
CarbonCaretDown: typeof import('~icons/carbon/caret-down')['default']
CilColumns: typeof import('~icons/cil/columns')['default']
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']
MdiLightChevronDoubleDown: typeof import('~icons/mdi-light/chevron-double-down')['default']
MdiLightChevronLeft: typeof import('~icons/mdi-light/chevron-left')['default']
MdiLightChevronRight: typeof import('~icons/mdi-light/chevron-right')['default']
MdiLightCog: typeof import('~icons/mdi-light/cog')['default']
MdiLightMagnify: typeof import('~icons/mdi-light/magnify')['default']
MobileMenu: typeof import('./components/MobileMenu.vue')['default']
OcticonContainer24: typeof import('~icons/octicon/container24')['default']
OcticonDownload24: typeof import('~icons/octicon/download24')['default']
OcticonTrash24: typeof import('~icons/octicon/trash24')['default']
PastTime: typeof import('./components/PastTime.vue')['default']
RelativeTime: typeof import('./components/RelativeTime.vue')['default']
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,37 @@
{{ 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";
import { formatBytes } from "@/utils";
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,
});
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.has-spacer {
&::after {
content: " ";
}
}
</style>

View File

@@ -1,18 +1,21 @@
<template>
<div class="columns is-marginless has-text-weight-bold is-family-monospace">
<span class="column is-ellipsis"
>{{ container.name }} <span class="tag is-dark">{{ container.image }}</span></span
>
<span class="column is-ellipsis">
{{ container.name }}
<span class="tag is-dark">{{ container.image.replace(/@sha.*/, "") }}</span>
</span>
</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,101 +11,96 @@
: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"><icon name="crate"></icon></span>
<span class="icon is-small" :class="props.option.state">
<octicon-container-24 />
</span>
</div>
<div class="media-content">
{{ props.option.name }}
</div>
<div class="media-right">
<span class="icon is-small column-icon" @click.stop.prevent="addColumn(props.option)" title="Pin as column">
<icon name="column"></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 Icon from "./Icon";
const props = defineProps({
maxResults: {
default: 20,
type: Number,
},
});
export default {
props: {
maxResults: {
default: 20,
type: Number,
},
},
data() {
return {
query: "",
};
},
name: "FuzzySearchModal",
components: {
Icon,
PastTime,
},
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 } });
emit("close");
}
function addColumn(container: Container) {
store.appendActiveContainer(container);
emit("close");
}
</script>
<style lang="scss" scoped>
.panel {
min-height: 400px;
width: 580px;
}
.running {
@@ -122,7 +117,7 @@ export default {
}
}
::v-deep a.dropdown-item {
:deep(a.dropdown-item) {
padding-right: 1em;
.media-right {
visibility: hidden;
@@ -131,4 +126,8 @@ export default {
visibility: visible;
}
}
.icon {
vertical-align: middle;
}
</style>

View File

@@ -1,32 +0,0 @@
<template functional>
<svg class="icomoon" :class="['icon-' + props.name]">
<use :href="'#icon-' + props.name"></use>
</svg>
</template>
<script>
export default {
props: {
name: {
required: true,
type: String,
},
},
name: "Icon",
};
</script>
<style lang="scss" scoped>
.icomoon {
display: inline-block;
width: 1em;
height: 1em;
stroke-width: 0;
stroke: currentColor;
fill: currentColor;
.icon:not(.keep-size) & {
width: 100%;
height: 100%;
}
}
</style>

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

@@ -0,0 +1,86 @@
<template>
<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 lang="ts" setup>
import { onMounted, onUnmounted, PropType } from "vue";
import hotkeys from "hotkeys-js";
import config from "@/stores/config";
import { Container } from "@/types/Container";
import { useSearchFilter } from "@/composables/search";
const { showSearch } = useSearchFilter();
const { base } = config;
const props = defineProps({
onClearClicked: {
type: Function as PropType<(e: Event) => void>,
default: (e: Event) => {},
},
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 lang="scss" scoped>
#download.button,
#clear.button {
.icon {
height: 80%;
}
&:hover {
color: var(--primary-color);
border-color: var(--primary-color);
}
}
</style>

View File

@@ -1,79 +1,64 @@
<template>
<scrollable-view :scrollable="scrollable" v-if="container">
<template v-slot:header v-if="showTitle">
<div class="mr-0 columns is-vcentered is-hidden-mobile">
<div class="column is-clipped">
<container-title :container="container" @close="$emit('close')"></container-title>
<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')" />
</div>
<div class="column is-clipped">
<container-stat :stat="container.stat" :state="container.state"></container-stat>
<div class="column is-narrow is-paddingless">
<container-stat :stat="container.stat" :state="container.state" v-if="container.stat" />
</div>
<div class="column is-narrow">
<a
class="button is-small is-outlined"
id="download"
:href="`${base}/api/logs/download?id=${container.id}`"
download
>
<span class="icon">
<icon name="save"></icon>
</span>
Download
</a>
<div class="mr-2 column is-narrow is-paddingless">
<log-actions-toolbar :container="container" :onClearClicked="onClearClicked" />
</div>
<div class="column is-narrow" v-if="closable">
<button class="delete is-medium" @click="$emit('close')"></button>
<div class="mr-2 column is-narrow is-paddingless" v-if="closable">
<button class="delete is-medium" @click="emit('close')"></button>
</div>
</div>
</template>
<template v-slot="{ setLoading }">
<log-viewer-with-source :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 ScrollableView from "./ScrollableView";
import ContainerTitle from "./ContainerTitle";
import ContainerStat from "./ContainerStat";
import Icon from "./Icon";
import config from "../store/config";
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,
ScrollableView,
ContainerTitle,
ContainerStat,
Icon,
showTitle: {
type: Boolean,
default: false,
},
computed: {
base() {
return config.base;
},
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 {
@@ -88,16 +73,4 @@ button.delete {
opacity: 1;
}
}
#download.button {
.icon {
margin-right: 5px;
height: 80%;
}
&:hover {
color: var(--primary-color);
border-color: var(--primary-color);
}
}
</style>

View File

@@ -1,81 +1,109 @@
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";
import { createRouter, createWebHistory } from "vue-router";
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 = "",
hourStyle = "auto",
}: { searchFilter?: string | undefined; hourStyle?: "auto" | "24" | "12" } = {
hourStyle: "auto",
}
) {
settings.value.hourStyle = hourStyle;
search.searchFilter.value = searchFilter;
const router = createRouter({
history: createWebHistory("/"),
routes: [
{
path: "/",
component: {
template: "Test from createLogEventSource",
},
},
],
});
return mount(LogEventSource, {
localVue,
store,
scopedSlots: {
global: {
plugins: [router, 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 +118,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 +130,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 +139,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 +173,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 +184,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 +195,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 +206,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 +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-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 +231,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,122 +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 = [];
},
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,84 +1,123 @@
<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" ref="events" :class="{ 'disable-wrap': !softWrap, [size]: true }">
<li
v-for="(item, index) in filtered"
:key="item.key"
:data-key="item.key"
:data-event="item.event"
:class="{ selected: item.selected }"
>
<div class="line-options" v-show="isSearching()">
<dropdown-menu :class="{ 'is-last': index === filtered.length - 1 }" class="is-top minimal">
<a class="dropdown-item" @click="handleJumpLineSelected($event, item)" :href="`#${item.key}`">
<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 } from "vue";
import { useRouteHash } from "@vueuse/router";
import { size, showTimestamp, softWrap } 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";
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 }),
});
const { filteredMessages, resetSearch, markSearch, isSearching } = useSearchFilter();
const colorize = (value: string) => markSearch(ansiConvertor.toHtml(value));
const { messages } = toRefs(props);
const filtered = filteredMessages(messages);
const events = ref<HTMLElement>();
let lastSelectedItem: LogEntry | undefined = undefined;
function handleJumpLineSelected(e: Event, item: LogEntry) {
if (lastSelectedItem) {
lastSelectedItem.selected = false;
}
lastSelectedItem = item;
item.selected = true;
resetSearch();
}
export default {
props: ["messages"],
name: "LogViewer",
components: { RelativeTime },
data() {
return {
showSearch: false,
};
const routeHash = useRouteHash();
watch(
routeHash,
(hash) => {
document.querySelector(`[data-key="${hash.substring(1)}"]`)?.scrollIntoView({ block: "center" });
},
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;
},
},
};
{ immediate: true, flush: "post" }
);
</script>
<style scoped lang="scss">
.events {
padding: 1em;
padding: 1em 0;
font-family: SFMono-Regular, Consolas, Liberation Mono, monaco, Menlo, monospace;
&.disable-wrap {
.line,
.text {
white-space: nowrap;
}
}
& > li {
display: flex;
word-wrap: break-word;
line-height: 130%;
padding: 0.2em 1em;
&:last-child {
scroll-snap-align: end;
scroll-margin-block-end: 5rem;
}
&[data-event="container-stopped"] {
&:nth-child(odd) {
background-color: rgba(125, 125, 125, 0.08);
}
&[data-event="container-stopped"] {
color: #f14668;
}
&[data-event="container-started"] {
color: hsl(141, 53%, 53%);
}
&.selected .date {
background-color: var(--menu-item-active-background-color);
color: var(--text-color);
}
&.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 +133,48 @@ 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;
border-radius: 3px;
}
.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,19 +1,26 @@
<template>
<log-event-source :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";
export default {
props: ["id"],
name: "LogViewerWithSource",
components: {
LogEventSource,
LogViewer,
<script lang="ts" setup>
import LogViewer from "./LogViewer.vue";
import { ref } from "vue";
defineProps({
id: {
type: String,
required: true,
},
};
});
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">
@@ -27,11 +27,7 @@
<p class="menu-label is-hidden-mobile" :class="{ 'is-active': showNav }">Containers</p>
<ul class="menu-list is-hidden-mobile" :class="{ 'is-active': showNav }">
<li v-for="item in visibleContainers" :key="item.id">
<router-link
:to="{ name: 'container', params: { id: item.id, name: item.name } }"
active-class="is-active"
:title="item.name"
>
<router-link :to="{ name: 'container', params: { id: item.id } }" active-class="is-active" :title="item.name">
<div class="is-ellipsis">
{{ item.name }}
</div>
@@ -41,32 +37,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,79 +17,66 @@
</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 {
display: inline-block;
position: relative;
pointer-events: none;
svg {
filter: drop-shadow(0px 1px 1px rgba(0, 0, 0, 0.2));
@@ -108,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,28 +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" :class="hasMore ? 'has-more' : ''" @click="scrollToBottom('instant')" v-show="paused">
<icon name="chevrons-down"></icon>
<mdi-light-chevron-double-down />
</button>
</transition>
</div>
</section>
</template>
<script>
import Icon from "./Icon";
import ScrollProgress from "./ScrollProgress";
<script lang="ts">
export default {
props: {
scrollable: {
@@ -32,21 +32,20 @@ export default {
default: true,
},
},
components: {
Icon,
ScrollProgress,
},
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 {
@@ -58,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 });
@@ -90,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">
<icon name="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 Icon from "./Icon";
export default {
props: [],
name: "Search",
components: {
Icon,
},
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,36 +9,24 @@
</router-link>
</div>
<div class="column is-narrow has-text-right px-1">
<button
class="button is-small is-rounded is-settings-control"
@click="$emit('search')"
title="Search containers (⌘ + k, ⌃k)"
>
<button class="button is-rounded" @click="$emit('search')" title="Search containers (⌘ + k, ⌃k)">
<span class="icon">
<icon name="search"></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"
>
<router-link :to="{ name: 'settings' }" active-class="is-active" class="button is-rounded">
<span class="icon">
<icon name="cog"></icon>
<mdi-light-cog />
</span>
</router-link>
</div>
</div>
<p class="menu-label is-hidden-mobile">Containers</p>
<ul class="menu-list is-hidden-mobile">
<ul class="menu-list is-hidden-mobile" v-if="ready">
<li v-for="item in visibleContainers" :key="item.id" :class="item.state">
<router-link
:to="{ name: 'container', params: { id: item.id, name: item.name } }"
active-class="is-active"
:title="item.name"
>
<router-link :to="{ name: 'container', params: { id: item.id } }" active-class="is-active" :title="item.name">
<div class="container is-flex is-align-items-center">
<div class="is-flex-grow-1 is-ellipsis">
{{ item.name }}
@@ -46,49 +34,39 @@
<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"
>
<icon name="column"></icon>
<cil-columns />
</span>
</div>
</div>
</router-link>
</li>
</ul>
<ul class="menu-list is-hidden-mobile loading" v-else>
<li v-for="index in 7" class="my-4"><o-skeleton animated size="large"></o-skeleton></li>
</ul>
</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 Icon from "./Icon";
const store = useContainerStore();
export default {
props: [],
name: "SideMenu",
components: {
Icon,
},
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, ready } = 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 {
@@ -103,6 +81,10 @@ aside {
}
}
.loading {
opacity: 0.5;
}
li.exited a {
color: #777;
}
@@ -116,6 +98,10 @@ li.exited a {
.menu-list li {
.column-icon {
visibility: hidden;
& > span {
vertical-align: middle;
}
}
&:hover .column-icon {

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,201 @@
// 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=\\"\\">
<div class=\\"line-options\\" data-v-28f125ea=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-3af6a38b=\\"\\" data-v-28f125ea=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-3af6a38b=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-3af6a38b=\\"\\"><span class=\\"icon\\" data-v-3af6a38b=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-3af6a38b=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-3af6a38b=\\"\\">
<div class=\\"dropdown-content\\" data-v-3af6a38b=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2019-06-12T23:55:42.459034602Z\\" data-v-28f125ea=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-28f125ea=\\"\\">
<div class=\\"level-left\\" data-v-28f125ea=\\"\\">
<div class=\\"level-item\\" data-v-28f125ea=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-28f125ea=\\"\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-28f125ea=\\"\\">
<div class=\\"level-item\\" data-v-28f125ea=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<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=\\"\\">
<div class=\\"line-options\\" data-v-28f125ea=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-3af6a38b=\\"\\" data-v-28f125ea=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-3af6a38b=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-3af6a38b=\\"\\"><span class=\\"icon\\" data-v-3af6a38b=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-3af6a38b=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-3af6a38b=\\"\\">
<div class=\\"dropdown-content\\" data-v-3af6a38b=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2019-06-12T23:55:42.459034602Z\\" data-v-28f125ea=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-28f125ea=\\"\\">
<div class=\\"level-left\\" data-v-28f125ea=\\"\\">
<div class=\\"level-item\\" data-v-28f125ea=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-28f125ea=\\"\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-28f125ea=\\"\\">
<div class=\\"level-item\\" data-v-28f125ea=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<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=\\"\\">
<div class=\\"line-options\\" data-v-28f125ea=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-3af6a38b=\\"\\" data-v-28f125ea=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-3af6a38b=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-3af6a38b=\\"\\"><span class=\\"icon\\" data-v-3af6a38b=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-3af6a38b=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-3af6a38b=\\"\\">
<div class=\\"dropdown-content\\" data-v-3af6a38b=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2019-06-12T10:55:42.459034602Z\\" data-v-28f125ea=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-28f125ea=\\"\\">
<div class=\\"level-left\\" data-v-28f125ea=\\"\\">
<div class=\\"level-item\\" data-v-28f125ea=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-28f125ea=\\"\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-28f125ea=\\"\\">
<div class=\\"level-item\\" data-v-28f125ea=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<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=\\"\\">
<div class=\\"line-options\\" data-v-28f125ea=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-3af6a38b=\\"\\" data-v-28f125ea=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-3af6a38b=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-3af6a38b=\\"\\"><span class=\\"icon\\" data-v-3af6a38b=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-3af6a38b=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-3af6a38b=\\"\\">
<div class=\\"dropdown-content\\" data-v-3af6a38b=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2019-06-12T10:55:42.459034602Z\\" data-v-28f125ea=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-28f125ea=\\"\\">
<div class=\\"level-left\\" data-v-28f125ea=\\"\\">
<div class=\\"level-item\\" data-v-28f125ea=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-28f125ea=\\"\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-28f125ea=\\"\\">
<div class=\\"level-item\\" data-v-28f125ea=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<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=\\"\\">
<div class=\\"line-options\\" data-v-28f125ea=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-3af6a38b=\\"\\" data-v-28f125ea=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-3af6a38b=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-3af6a38b=\\"\\"><span class=\\"icon\\" data-v-3af6a38b=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-3af6a38b=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-3af6a38b=\\"\\">
<div class=\\"dropdown-content\\" data-v-3af6a38b=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2019-06-12T10:55:42.459034602Z\\" data-v-28f125ea=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-28f125ea=\\"\\">
<div class=\\"level-left\\" data-v-28f125ea=\\"\\">
<div class=\\"level-item\\" data-v-28f125ea=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-28f125ea=\\"\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-28f125ea=\\"\\">
<div class=\\"level-item\\" data-v-28f125ea=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<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=\\"\\">
<div class=\\"line-options\\" data-v-28f125ea=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-3af6a38b=\\"\\" data-v-28f125ea=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-3af6a38b=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-3af6a38b=\\"\\"><span class=\\"icon\\" data-v-3af6a38b=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-3af6a38b=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-3af6a38b=\\"\\">
<div class=\\"dropdown-content\\" data-v-3af6a38b=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2019-06-12T10:55:42.459034602Z\\" data-v-28f125ea=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-28f125ea=\\"\\">
<div class=\\"level-left\\" data-v-28f125ea=\\"\\">
<div class=\\"level-item\\" data-v-28f125ea=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-28f125ea=\\"\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-28f125ea=\\"\\">
<div class=\\"level-item\\" data-v-28f125ea=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<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,56 @@
import { ref, computed, Ref } from "vue";
const searchFilter = ref<string>("");
const showSearch = ref(false);
import type { LogEntry } from "@/types/LogEntry";
export function useSearchFilter() {
const regex = computed(() => {
const isSmartCase = searchFilter.value === searchFilter.value.toLowerCase();
return isSmartCase ? new RegExp(searchFilter.value, "i") : new RegExp(searchFilter.value);
});
function filteredMessages(messages: Ref<LogEntry[]>) {
return computed(() => {
if (searchFilter && searchFilter.value) {
try {
return messages.value.filter((d) => d.message.match(regex.value));
} catch (e) {
if (e instanceof SyntaxError) {
console.info(`Ignoring SyntaxError from search.`, e);
return messages.value;
}
throw e;
}
}
return messages.value;
});
}
function markSearch(log: string) {
if (searchFilter && searchFilter.value) {
return log.replace(regex.value, `<mark>$&</mark>`);
}
return log;
}
function resetSearch() {
searchFilter.value = "";
showSearch.value = false;
}
function isSearching() {
return showSearch.value && searchFilter.value;
}
return {
filteredMessages,
searchFilter,
showSearch,
markSearch,
resetSearch,
isSearching,
};
}

View File

@@ -0,0 +1,73 @@
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";
softWrap: boolean;
} = {
search: true,
size: "medium",
menuWidth: 15,
smallerScrollbars: false,
showTimestamp: true,
showAllContainers: false,
lightTheme: "auto",
hourStyle: "auto",
softWrap: true,
};
export const settings = useStorage(DOZZLE_SETTINGS_KEY, DEFAULT_SETTINGS);
settings.value = {...DEFAULT_SETTINGS, ...settings.value};
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),
});
export const softWrap = computed({
get: () => settings.value.softWrap,
set: (value) => (settings.value.softWrap = 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,72 +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 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(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");

68
assets/main.ts Normal file
View File

@@ -0,0 +1,68 @@
import "./styles.scss";
import { createApp } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import { Autocomplete, Button, Dropdown, Switch, Radio, Skeleton, 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(Skeleton)
.use(Config, bulmaConfig)
.mount("#app");

View File

@@ -1,47 +1,35 @@
<template>
<div>
<search></search>
<log-container :id="id" show-title :scrollable="activeContainers.length > 0"> </log-container>
</div>
<search></search>
<log-container :id="id" show-title :scrollable="activeContainers.length > 0"> </log-container>
</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

@@ -14,7 +14,7 @@
</div>
</div>
</section>
<section class="level section is-mobile">
<section class="level section">
<div class="level-item has-text-centered">
<div>
<p class="title">{{ containers.length }}</p>
@@ -27,6 +27,18 @@
<p class="heading">Running</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title" data-ci-skip>{{ totalCpu }}%</p>
<p class="heading">Total CPU Usage</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title" data-ci-skip>{{ formatBytes(totalMem) }}</p>
<p class="heading">Total Mem Usage</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title">{{ version }}</p>
@@ -50,7 +62,7 @@
@keyup.enter="onEnter()"
/>
<span class="icon is-left">
<icon name="search"></icon>
<search-icon />
</span>
</p>
</div>
@@ -76,60 +88,66 @@
</div>
</template>
<script>
import { mapState } from "vuex";
import Icon from "../components/Icon";
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 { formatBytes } from "@/utils";
import fuzzysort from "fuzzysort";
import SearchIcon from "~icons/mdi-light/magnify";
import PastTime from "../components/PastTime.vue";
import config from "@/stores/config";
import { useIntervalFn } from "@vueuse/core";
export default {
name: "Index",
components: { Icon, 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"));
const totalCpu = ref(0);
useIntervalFn(
() => {
totalCpu.value = runningContainers.value.reduce((acc, c) => acc + (c.stat?.cpu ?? 0), 0);
},
};
1000,
{ immediate: true }
);
const totalMem = ref(0);
useIntervalFn(
() => {
totalMem.value = runningContainers.value.reduce((acc, c) => acc + (c.stat?.memoryUsage ?? 0), 0);
},
1000,
{ immediate: true }
);
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>
@@ -22,50 +21,99 @@
<div class="has-underline">
<h2 class="title is-4">Display</h2>
</div>
<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>
</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.
</div>
</div>
<div class="item">
<b-switch v-model="smallerScrollbars"> Use smaller scrollbars </b-switch>
</div>
<div class="item">
<b-switch v-model="showTimestamp"> Show timestamps </b-switch>
</div>
<div class="item">
<o-switch v-model="smallerScrollbars"> Use smaller scrollbars </o-switch>
</div>
<div class="item">
<o-switch v-model="showTimestamp"> Show timestamps </o-switch>
</div>
<div class="item">
<o-switch v-model="softWrap"> Soft wrap lines</o-switch>
</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="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.
</div>
</div>
</div>
<div class="item">
<div class="columns is-vcentered">
<div class="column is-narrow">
<o-field>
<o-dropdown v-model="size" aria-role="list">
<template #trigger>
<o-button variant="primary" type="button">
<span class="is-capitalized">{{ size }}</span>
<span class="icon">
<carbon-caret-down />
</span>
</o-button>
</template>
<o-dropdown-item
:value="value"
aria-role="listitem"
v-for="value in ['small', 'medium', 'large']"
:key="value"
>
<span class="is-capitalized">{{ value }}</span>
</o-dropdown-item>
</o-dropdown>
</o-field>
</div>
<div class="column">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,78 +121,54 @@
</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 Icon from "../components/Icon";
import config from "../store/config";
import config from "@/stores/config";
import { setTitle } from "@/composables/title";
import {
search,
lightTheme,
smallerScrollbars,
showTimestamp,
hourStyle,
showAllContainers,
size,
softWrap,
} from "@/composables/settings";
export default {
props: [],
name: "Settings",
components: {
Icon,
},
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/amir20/dozzle/releases/latest");
if (response.ok) {
const release = await response.json();
hasUpdate.value = gt(release.tag_name, currentVersion);
nextRelease.value = release;
}
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,81 @@
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";
import { watchOnce } from "@vueuse/core";
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) => {
const event = JSON.parse((e as MessageEvent).data) as { actorId: string };
const container = allContainersById.value[event.actorId];
if (container) {
container.state = "dead";
}
},
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);
const ready = ref(false);
watchOnce(containers, () => (ready.value = true));
return {
containers,
activeContainerIds,
allContainersById,
visibleContainers,
activeContainers,
currentContainer,
appendActiveContainer,
removeActiveContainer,
ready,
};
});
// @ts-ignore
if (import.meta.hot) {
// @ts-ignore
import.meta.hot.accept(acceptHMRUpdate(useContainerStore, import.meta.hot));
}

View File

@@ -1,6 +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);
@@ -25,15 +24,22 @@ $panel-heading-color: var(--panel-heading-color);
$link: $turquoise;
$link-active: $grey-dark;
@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/_autocomplete";
$dark-toolbar-color: rgba($black-bis, 0.7);
$light-toolbar-color: rgba($grey-darker, 0.7);
html {
@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 "@oruga-ui/theme-bulma/dist/scss/components/skeleton.scss";
@import "splitpanes/dist/splitpanes.css";
html,
[data-theme="dark"] {
--scheme-main: #{$black};
--scheme-main-bis: #{$black-bis};
--scheme-main-ter: #{$black-ter};
@@ -46,6 +52,7 @@ html {
--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%);
@@ -59,6 +66,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};
@@ -72,6 +137,7 @@ html {
--secondary-color: #d8f0ca;
--body-background-color: #{$white-bis};
--action-toolbar-background-color: #{$light-toolbar-color};
--body-color: #{$grey-darker};
--menu-item-color: #{$grey-dark};
@@ -121,22 +187,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;
@@ -156,3 +206,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 status: string;
state: "created" | "running" | "exited" | "dead" | "paused" | "restarting";
stat?: ContainerStat;
}
export interface ContainerStat {
readonly id: string;
readonly cpu: number;
readonly memory: number;
readonly memoryUsage: number;
}

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

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

8
assets/utils/index.ts Normal file
View File

@@ -0,0 +1,8 @@
export 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];
}

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__

11
e2e/Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM cypress/included:9.6.1
RUN apt install curl && 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/"
}

4
e2e/cypress.json Normal file
View File

@@ -0,0 +1,4 @@
{
"fixturesFolder": false,
"projectId": "8cua4m"
}

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().replaceSkippedElements().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: 35 KiB

View File

@@ -0,0 +1,37 @@
// ***********************************************
// 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()));
});
Cypress.Commands.add("replaceSkippedElements", () => {
cy.window().then((win) => win.document.querySelectorAll("[data-ci-skip]").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')

45
e2e/docker-compose.yml Normal file
View File

@@ -0,0 +1,45 @@
version: "3.4"
services:
custom_base:
container_name: custom_base
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- DOZZLE_FILTER=name=custom_base
- DOZZLE_BASE=/foobarbase
- DOZZLE_NO_ANALYTICS=1
ports:
- "8080:8080"
build:
context: ..
dozzle:
container_name: dozzle
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- DOZZLE_FILTER=name=dozzle
- DOZZLE_NO_ANALYTICS=1
ports:
- "9090:8080"
build:
context: ..
cypress:
build:
context: .
working_dir: /e2e
volumes:
- ./cypress:/e2e/cypress
- ./cypress.json:/e2e/cypress.json
environment:
- CYPRESS_DOZZLE_DEFAULT=http://dozzle:8080/
- CYPRESS_CUSTOM_DEFAULT=http://custom_base:8080/foobarbase
- CYPRESS_RECORD_KEY=155c3cf8-b2dd-4f5e-9fb3-7635f5b79d4d
- COMMIT_INFO_BRANCH=${GITHUB_REF_NAME}
- COMMIT_INFO_AUTHOR=${GITHUB_ACTOR}
- COMMIT_INFO_SHA=${GITHUB_SHA}
- COMMIT_INFO_MESSAGE=${GIT_LOG_MESSAGE}
- COMMIT_INFO_REMOTE=https://github.com/amir20/dozzle
command: cypress run --record
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.5.4",
"cypress-image-snapshot": "^4.0.1"
}
}

1423
e2e/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

27
go.mod
View File

@@ -1,44 +1,43 @@
module github.com/amir20/dozzle
require (
github.com/Microsoft/go-winio v0.5.0 // indirect
github.com/alexflint/go-arg v1.4.2
github.com/Microsoft/go-winio v0.5.1 // indirect
github.com/alexflint/go-arg v1.4.3
github.com/beme/abide v0.0.0-20190723115211-635a09831760
github.com/containerd/containerd v1.5.5 // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v20.10.10+incompatible
github.com/docker/docker v20.10.16+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
golang.org/x/net v0.0.0-20210420072503-d25e30425868 // indirect
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83 // indirect
google.golang.org/grpc v1.40.0 // indirect
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
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)
require (
github.com/alexflint/go-scalar v1.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.5 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/text v0.3.6 // indirect
google.golang.org/protobuf v1.27.1 // indirect
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
gotest.tools/v3 v3.0.3 // indirect
)
go 1.17
go 1.18

752
go.sum

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

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,34 +0,0 @@
version: "3.4"
services:
custom_base:
container_name: custom_base
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- DOZZLE_FILTER=name=custom_base
- DOZZLE_BASE=/foobarbase
- DOZZLE_NO_ANALYTICS=1
build:
context: ..
dozzle:
container_name: dozzle
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- DOZZLE_FILTER=name=dozzle
- DOZZLE_NO_ANALYTICS=1
build:
context: ..
integration:
build:
context: .
command: yarn test
volumes:
- ./__tests__:/app/__tests__
environment:
- DEFAULT_URL=http://dozzle:8080/
- CUSTOM_URL=http://custom_base:8080/foobarbase
- DOZZLE_NO_ANALYTICS=1
depends_on:
- dozzle
- custom_base

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": "^10.4.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;

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