Compare commits

...

115 Commits

Author SHA1 Message Date
Amir Raminfar
79f0e2127a Release 4.0.1 2022-08-19 14:08:13 -07:00
Amir Raminfar
163b1c7e28 Updates modules 2022-08-19 12:13:58 -07:00
Amir Raminfar
db01579f04 Fixes tests and returns millis too (#1854) 2022-08-19 12:10:18 -07:00
Amir Raminfar
be7c154d6b Adds debounce for search 2022-08-18 18:57:39 -07:00
Amir Raminfar
b1bc706de2 Release 4.0.0 2022-08-17 12:39:41 -07:00
Amir Raminfar
40f5cb1301 Simplifies schema 2022-08-17 10:39:22 -07:00
Amir Raminfar
cedfbee983 Updates cypress (#1851) 2022-08-16 14:05:19 -07:00
Amir Raminfar
c835f51cc4 Support for JSON logs (#1759)
* WIP for using json all the time

* Updates to render

* adds a new component for json

* Updates styles

* Adds nesting

* Adds field list

* Adds expanding

* Adds new composable for event source

* Creates an add button

* Removes unused code

* Adds and removes fields with defaults

* Fixes jumping when adding new fields

* Returns JSON correctly

* Fixes little bugs

* Fixes js tests

* Adds vscode

* Fixes json buffer error

* Fixes extra line

* Fixes tests

* Fixes tests and adds support for search

* Refactors visible payload keys to a composable

* Fixes typescript errors and refactors

* Fixes visible keys by ComputedRef<Ref>

* Fixes search bugs

* Updates tests

* Fixes go tests

* Fixes scroll view

* Fixes vue tsc errors

* Fixes EOF error

* Fixes build error

* Uses application/ld+json

* Fixes arrays and records

* Marks for json too
2022-08-16 13:53:31 -07:00
Amir Raminfar
5ab06d5906 Updates modules 2022-08-15 15:06:24 -07:00
Amir Raminfar
d44316fa9c Adds mising snapshots 2022-08-15 13:01:19 -07:00
Amir Raminfar
6ef3da9abd Adds dark mode 2022-08-15 13:00:49 -07:00
Amir Raminfar
752495ed6f Cleans up dark mode 2022-08-15 12:43:27 -07:00
Amir Raminfar
8f895e40bc Adds snapshot tests 2022-08-15 11:56:57 -07:00
Amir Raminfar
cd9ddcf427 Release 3.13.1 2022-08-08 12:34:51 -07:00
Amir Raminfar
bbc7794006 Updates snapshots 2022-08-08 12:27:47 -07:00
Amir Raminfar
7dc37f130c Replaces last line return 2022-08-08 10:39:09 -07:00
Amir Raminfar
0711bc1c76 Fixes test 2022-08-08 09:37:24 -07:00
Amir Raminfar
0aa24386b2 Fixes line return bug and heartbeat to comment in SSE 2022-08-08 09:36:23 -07:00
kodiakhq[bot]
ca35b93671 Merge pull request #1843 from amir20/dependabot/github_actions/docker/build-push-action-3.1.1
Bump docker/build-push-action from 3.1.0 to 3.1.1
2022-08-08 09:24:57 +00:00
dependabot[bot]
a6220e4d38 Bump docker/build-push-action from 3.1.0 to 3.1.1
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3.1.0 to 3.1.1.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v3.1.0...v3.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-08 09:20:10 +00:00
Amir Raminfar
4ed64a7cce Release 3.13.0 2022-08-04 13:35:27 -07:00
Amir Raminfar
0f27e11084 Updates vue components with sass 2022-08-04 13:33:50 -07:00
Amir Raminfar
85eafc9c40 Tries to add 1 mircosecond to skip first log event (#1838) 2022-08-04 13:24:21 -07:00
Amir Raminfar
332cc384ea Adds a heartbeat for log stream (#1837) 2022-08-04 12:52:19 -07:00
kodiakhq[bot]
72fd31f85b Merge pull request #1833 from amir20/dependabot/docker/e2e/cypress/included-10.4.0
Bump cypress/included from 10.3.1 to 10.4.0 in /e2e
2022-08-03 09:39:07 +00:00
dependabot[bot]
a0ce370e9e Bump cypress/included from 10.3.1 to 10.4.0 in /e2e
Bumps cypress/included from 10.3.1 to 10.4.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-08-03 09:34:35 +00:00
kodiakhq[bot]
e823904865 Merge pull request #1832 from amir20/dependabot/docker/golang-1.19.0-alpine
Bump golang from 1.18.5-alpine to 1.19.0-alpine
2022-08-03 09:13:08 +00:00
dependabot[bot]
22bbfe1592 Bump golang from 1.18.5-alpine to 1.19.0-alpine
Bumps golang from 1.18.5-alpine to 1.19.0-alpine.

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-03 09:08:55 +00:00
kodiakhq[bot]
770e1818f0 Merge pull request #1830 from amir20/dependabot/docker/golang-1.18.5-alpine
Bump golang from 1.18.4-alpine to 1.18.5-alpine
2022-08-02 09:19:12 +00:00
dependabot[bot]
d6fab75f8f Bump golang from 1.18.4-alpine to 1.18.5-alpine
Bumps golang from 1.18.4-alpine to 1.18.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>
2022-08-02 09:15:41 +00:00
Amir Raminfar
17c18c156e Release 3.12.14 2022-08-01 15:32:41 -07:00
Amir Raminfar
5eca19840e Fixes cpu count using online cpus. Fixes #1829 2022-08-01 13:32:48 -07:00
Amir Raminfar
b1d7b8ba55 Updates modules 2022-07-31 19:01:21 -07:00
Amir Raminfar
e2ee430bbd Updates modules 2022-07-26 09:48:21 -07:00
Amir Raminfar
0755a71dc2 Adds healthcheck to readme 2022-07-25 09:21:20 -07:00
Amir Raminfar
60758db9c8 Updates modules 2022-07-25 09:10:38 -07:00
kodiakhq[bot]
7b96196904 Merge pull request #1826 from amir20/dependabot/docker/e2e/cypress/included-10.3.1
Bump cypress/included from 10.3.0 to 10.3.1 in /e2e
2022-07-25 09:55:01 +00:00
dependabot[bot]
efcfa0e375 Bump cypress/included from 10.3.0 to 10.3.1 in /e2e
Bumps cypress/included from 10.3.0 to 10.3.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-07-25 09:50:54 +00:00
Amir Raminfar
66f9204ae6 Release 3.12.13 2022-07-24 18:40:17 -07:00
Amir Raminfar
73c023ce22 Disables healthcheck. Fixes #1819 (#1822) 2022-07-24 13:19:03 -07:00
Amir Raminfar
261517ac3f Updates modules 2022-07-22 08:28:19 -07:00
Amir Raminfar
2e0a546aa2 Release 3.12.12 2022-07-21 16:50:42 -07:00
Amir Raminfar
72ed7b50ba Adds platforms back for dev 2022-07-21 16:49:04 -07:00
Amir Raminfar
486bcec363 Revert "Updates with UPX with cross compile (#1817)"
This reverts commit 400cef767f.
2022-07-21 16:48:38 -07:00
Amir Raminfar
3db0ad42fe Removes python from Dockerfile 2022-07-21 14:57:17 -07:00
Amir Raminfar
c1a75e21ba Remove util linux 2022-07-21 14:56:10 -07:00
Amir Raminfar
96c5e24501 Removes make, ssh and g++ 2022-07-21 14:55:17 -07:00
Amir Raminfar
c1a16fd76e Removes git from Dockerfile 2022-07-21 14:53:15 -07:00
Amir Raminfar
42fab58c9f Release 3.12.11 2022-07-21 14:45:53 -07:00
Amir Raminfar
400cef767f Updates with UPX with cross compile (#1817)
* Revert "Removes UXP"

* Updates UPX again to be cross-compile
2022-07-21 09:53:55 -07:00
Amir Raminfar
84ae558467 Release 3.12.10 2022-07-20 15:57:05 -07:00
Amir Raminfar
0ebc9c562a Fixes deploy build 2022-07-20 15:56:23 -07:00
Amir Raminfar
f67664470f Adds COMPOSE_DOCKER_CLI_BUILD 2022-07-20 15:54:50 -07:00
Amir Raminfar
1f811da273 Fixes build file 2022-07-20 15:41:06 -07:00
Amir Raminfar
fdfc9fceba Uses cross compilation for faster builds 2022-07-20 15:36:48 -07:00
Amir Raminfar
5b5b741b68 Release 3.12.9 2022-07-20 14:55:01 -07:00
Amir Raminfar
18c88d0e85 Removes UXP 2022-07-20 14:45:09 -07:00
Amir Raminfar
1603a19538 Release 3.12.8 2022-07-20 12:33:39 -07:00
Amir Raminfar
5cffa287d5 Adds a simple healthcheck every 2s (#1814)
* Adds a simple healthcheck every 5s

* Fixes bugs for healthcheck
2022-07-20 11:56:42 -07:00
kodiakhq[bot]
93f57b6e90 Merge pull request #1815 from amir20/dependabot/github_actions/docker/build-push-action-3.1.0
Bump docker/build-push-action from 3.0.0 to 3.1.0
2022-07-20 09:27:42 +00:00
kodiakhq[bot]
2346f6a0eb Merge pull request #1816 from amir20/dependabot/go_modules/github.com/spf13/afero-1.9.2
Bump github.com/spf13/afero from 1.9.0 to 1.9.2
2022-07-20 09:27:29 +00:00
dependabot[bot]
f95317ac1d Bump github.com/spf13/afero from 1.9.0 to 1.9.2
Bumps [github.com/spf13/afero](https://github.com/spf13/afero) from 1.9.0 to 1.9.2.
- [Release notes](https://github.com/spf13/afero/releases)
- [Commits](https://github.com/spf13/afero/compare/v1.9.0...v1.9.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-07-20 09:22:47 +00:00
dependabot[bot]
157a612f34 Bump docker/build-push-action from 3.0.0 to 3.1.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3.0.0 to 3.1.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v3.0.0...v3.1.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-07-20 09:22:16 +00:00
kodiakhq[bot]
42c890ad50 Merge pull request #1813 from amir20/dependabot/go_modules/github.com/sirupsen/logrus-1.9.0
Bump github.com/sirupsen/logrus from 1.8.1 to 1.9.0
2022-07-19 09:14:04 +00:00
dependabot[bot]
48638a18f2 Bump github.com/sirupsen/logrus from 1.8.1 to 1.9.0
Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.8.1 to 1.9.0.
- [Release notes](https://github.com/sirupsen/logrus/releases)
- [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sirupsen/logrus/compare/v1.8.1...v1.9.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-19 09:09:54 +00:00
Amir Raminfar
fae0640bba Updates modules 2022-07-18 08:55:19 -07:00
Amir Raminfar
23b37bb912 Uses UPX to shrink binary (#1809)
* Tries to use UPX

* Uses harshavardhanj/upx instead
2022-07-17 10:22:23 -07:00
kodiakhq[bot]
07135fea91 Merge pull request #1808 from amir20/dependabot/go_modules/github.com/spf13/afero-1.9.0
Bump github.com/spf13/afero from 1.8.2 to 1.9.0
2022-07-15 09:14:16 +00:00
dependabot[bot]
1a3c394fe4 Bump github.com/spf13/afero from 1.8.2 to 1.9.0
Bumps [github.com/spf13/afero](https://github.com/spf13/afero) from 1.8.2 to 1.9.0.
- [Release notes](https://github.com/spf13/afero/releases)
- [Commits](https://github.com/spf13/afero/compare/v1.8.2...v1.9.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-07-15 09:10:20 +00:00
Amir Raminfar
38ec37ed19 Updates modules 2022-07-14 11:14:12 -07:00
kodiakhq[bot]
738ae98f2f Merge pull request #1806 from amir20/dependabot/docker/golang-1.18.4-alpine
Bump golang from 1.18.3-alpine to 1.18.4-alpine
2022-07-13 09:12:37 +00:00
dependabot[bot]
99d1e83882 Bump golang from 1.18.3-alpine to 1.18.4-alpine
Bumps golang from 1.18.3-alpine to 1.18.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>
2022-07-13 09:08:26 +00:00
Amir Raminfar
d71be7e239 Updates modules 2022-07-08 10:52:36 -07:00
kodiakhq[bot]
c0b9325efb Merge pull request #1791 from amir20/dependabot/go_modules/github.com/stretchr/testify-1.8.0
Bump github.com/stretchr/testify from 1.7.5 to 1.8.0
2022-06-30 09:15:40 +00:00
dependabot[bot]
538fe6f158 Bump github.com/stretchr/testify from 1.7.5 to 1.8.0
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.7.5 to 1.8.0.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.7.5...v1.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-30 09:12:01 +00:00
Amir Raminfar
965d1a52b1 Updates cypress 2022-06-29 13:30:40 -07:00
Amir Raminfar
6be73692ba Release 3.12.7 2022-06-28 15:21:10 -07:00
Amir Raminfar
f694c168d3 Updates cypress 2022-06-27 09:17:22 -07:00
Amir Raminfar
b7c24dcafa Updates modules 2022-06-27 09:16:18 -07:00
kodiakhq[bot]
67a1c4a207 Merge pull request #1785 from amir20/dependabot/go_modules/github.com/stretchr/testify-1.7.5
Bump github.com/stretchr/testify from 1.7.4 to 1.7.5
2022-06-24 09:17:06 +00:00
dependabot[bot]
b188f689ea Bump github.com/stretchr/testify from 1.7.4 to 1.7.5
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.7.4 to 1.7.5.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.7.4...v1.7.5)

---
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-06-24 09:11:31 +00:00
Amir Raminfar
6f354c500c Updates test snapshots 2022-06-23 11:34:04 -07:00
Amir Raminfar
8ba5d36801 Vite beta 2022-06-23 11:24:22 -07:00
Amir Raminfar
6822a95cc9 Updates modules 2022-06-23 10:08:54 -07:00
kodiakhq[bot]
fcc4647379 Merge pull request #1780 from amir20/dependabot/go_modules/github.com/stretchr/testify-1.7.4
Bump github.com/stretchr/testify from 1.7.2 to 1.7.4
2022-06-21 09:26:10 +00:00
dependabot[bot]
0305ee9502 Bump github.com/stretchr/testify from 1.7.2 to 1.7.4
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.7.2 to 1.7.4.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.7.2...v1.7.4)

---
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-06-21 09:09:34 +00:00
kodiakhq[bot]
0e527e8ec0 Merge pull request #1773 from amir20/dependabot/docker/e2e/cypress/included-10.0.3
Bump cypress/included from 10.0.2 to 10.0.3 in /e2e
2022-06-07 09:44:11 +00:00
dependabot[bot]
91b2dc36c2 Bump cypress/included from 10.0.2 to 10.0.3 in /e2e
Bumps cypress/included from 10.0.2 to 10.0.3.

---
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-06-07 09:39:25 +00:00
kodiakhq[bot]
3dc7949a86 Merge pull request #1772 from amir20/dependabot/go_modules/github.com/stretchr/testify-1.7.2
Bump github.com/stretchr/testify from 1.7.1 to 1.7.2
2022-06-07 09:14:30 +00:00
kodiakhq[bot]
5cf625ef65 Merge pull request #1771 from amir20/dependabot/go_modules/github.com/docker/docker-20.10.17incompatible
Bump github.com/docker/docker from 20.10.16+incompatible to 20.10.17+incompatible
2022-06-07 09:13:46 +00:00
dependabot[bot]
8d5deff2ed Bump github.com/stretchr/testify from 1.7.1 to 1.7.2
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.7.1 to 1.7.2.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.7.1...v1.7.2)

---
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-06-07 09:09:55 +00:00
dependabot[bot]
9f6df9a25a Bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 20.10.16+incompatible to 20.10.17+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.16...v20.10.17)

---
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-06-07 09:09:46 +00:00
Amir Raminfar
a34733bc88 Updates modules 2022-06-06 09:58:02 -07:00
kodiakhq[bot]
7cf02f40e6 Merge pull request #1768 from amir20/dependabot/docker/e2e/cypress/included-10.0.2
Bump cypress/included from 10.0.1 to 10.0.2 in /e2e
2022-06-03 10:26:06 +00:00
dependabot[bot]
9d2e87f0f3 Bump cypress/included from 10.0.1 to 10.0.2 in /e2e
Bumps cypress/included from 10.0.1 to 10.0.2.

---
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-06-03 10:21:47 +00:00
Amir Raminfar
034984a784 Updates modules 2022-06-02 15:10:48 -07:00
kodiakhq[bot]
d11fcdfec5 Merge pull request #1764 from amir20/dependabot/docker/e2e/cypress/included-10.0.1
Bump cypress/included from 9.7.0 to 10.0.1 in /e2e
2022-06-02 18:49:29 +00:00
Vano Devium
af08b5cd1b Auto session storage key (#1767) 2022-06-02 11:44:32 -07:00
Amir Raminfar
c666917740 Adds missing files 2022-06-02 11:31:51 -07:00
Amir Raminfar
059c3361ca Updates to v10 and disables snapshotting for now 2022-06-02 11:31:28 -07:00
Amir Raminfar
f18fdcec8c Updates to v10 2022-06-02 10:07:14 -07:00
Vano Devium
18d6aa2a34 Removed redundant http.HandlerFunc() casting (#1765) 2022-06-02 06:54:22 -07:00
dependabot[bot]
db4643d271 Bump cypress/included from 9.7.0 to 10.0.1 in /e2e
Bumps cypress/included from 9.7.0 to 10.0.1.

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-02 09:38:49 +00:00
kodiakhq[bot]
9d5b6faf03 Merge pull request #1762 from amir20/dependabot/docker/golang-1.18.3-alpine
Bump golang from 1.18.2-alpine to 1.18.3-alpine
2022-06-02 09:13:50 +00:00
dependabot[bot]
72e0a1ba2d Bump golang from 1.18.2-alpine to 1.18.3-alpine
Bumps golang from 1.18.2-alpine to 1.18.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>
2022-06-02 09:10:15 +00:00
Amir Raminfar
35d4f3c8d3 Updates modules 2022-05-31 21:05:01 -07:00
kodiakhq[bot]
6dfafbf531 Merge pull request #1758 from amir20/dependabot/github_actions/pnpm/action-setup-2.2.2
Bump pnpm/action-setup from 2.2.1 to 2.2.2
2022-05-30 09:16:16 +00:00
dependabot[bot]
11e7717519 Bump pnpm/action-setup from 2.2.1 to 2.2.2
Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 2.2.1 to 2.2.2.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v2.2.1...v2.2.2)

---
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-05-30 09:12:40 +00:00
kodiakhq[bot]
a4539399d2 Merge pull request #1754 from amir20/dependabot/docker/e2e/cypress/included-9.7.0
Bump cypress/included from 9.6.1 to 9.7.0 in /e2e
2022-05-24 17:49:59 +00:00
Amir Raminfar
d14be81f18 Updates cypress 2022-05-24 10:46:38 -07:00
Amir Raminfar
44c4366bba Updates cypress 2022-05-24 10:36:37 -07:00
dependabot[bot]
20b115f99f Bump cypress/included from 9.6.1 to 9.7.0 in /e2e
Bumps cypress/included from 9.6.1 to 9.7.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-05-24 09:47:31 +00:00
Amir Raminfar
e99ba5b6ae Updates modules 2022-05-23 12:12:36 -07:00
Amir Raminfar
12d32ee8f2 Updates modules 2022-05-18 10:52:13 -07:00
Amir Raminfar
0f423e8b60 Fixes space 2022-05-17 14:00:31 -07:00
65 changed files with 2690 additions and 2317 deletions

View File

@@ -13,7 +13,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v3
- name: Install pnpm
uses: pnpm/action-setup@v2.2.1
uses: pnpm/action-setup@v2.2.2
with:
version: 6.20.1
- name: Install dependencies
@@ -39,9 +39,9 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
- name: Build images
run: docker-compose -f e2e/docker-compose.yml build
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml build
- name: Run tests
run: docker-compose -f e2e/docker-compose.yml up --build --force-recreate --exit-code-from cypress
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml up --build --force-recreate --exit-code-from cypress
buildx:
needs: [go-test, npm-test, int-test]
name: Release
@@ -62,7 +62,7 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v3.0.0
uses: docker/build-push-action@v3.1.1
with:
push: true
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
@@ -83,7 +83,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v3
- name: Install pnpm
uses: pnpm/action-setup@v2.2.1
uses: pnpm/action-setup@v2.2.2
with:
version: 6.20.1
- name: Install dependencies

View File

@@ -27,10 +27,10 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v3.0.0
uses: docker/build-push-action@v3.1.1
with:
push: true
platforms: linux/amd64
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }}
build-args: TAG=${{ steps.meta.outputs.version }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -16,7 +16,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v3
- name: Install pnpm
uses: pnpm/action-setup@v2.2.1
uses: pnpm/action-setup@v2.2.2
with:
version: 6.20.1
- name: Install dependencies
@@ -43,8 +43,12 @@ jobs:
uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.0.0
- name: Build images
run: docker-compose -f e2e/docker-compose.yml build
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml build
- name: Set commit message for push
if: github.event_name == 'push'
run: |
@@ -58,4 +62,4 @@ jobs:
git log -1 --pretty=%B ${{github.event.pull_request.head.sha}} >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Run tests
run: docker-compose -f e2e/docker-compose.yml up --build --force-recreate --exit-code-from cypress
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml up --build --force-recreate --exit-code-from cypress

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ static
dozzle
coverage
.pnpm-debug.log
.vscode

View File

@@ -1,7 +1,7 @@
# Build assets
FROM node:18-alpine as node
FROM --platform=$BUILDPLATFORM node:18-alpine as node
RUN apk add --no-cache git openssh make g++ util-linux python3 && npm install -g pnpm
RUN npm install -g pnpm
WORKDIR /build
@@ -19,9 +19,9 @@ COPY assets ./assets
# Install dependencies
RUN pnpm install -r --offline --prod --ignore-scripts && pnpm build
FROM golang:1.18.2-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.19.0-alpine AS builder
RUN apk add --no-cache git ca-certificates && mkdir /dozzle
RUN apk add --no-cache ca-certificates && mkdir /dozzle
WORKDIR /dozzle
@@ -34,15 +34,18 @@ COPY --from=node /build/dist ./dist
# Copy all other files
COPY analytics ./analytics
COPY healthcheck ./healthcheck
COPY docker ./docker
COPY web ./web
COPY main.go ./
# Args
ARG TAG=dev
ARG TARGETOS TARGETARCH
# Build binary
RUN CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=$TAG" -o dozzle
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=$TAG" -o dozzle
FROM scratch

View File

@@ -58,6 +58,30 @@ Dozzle will be available at [http://localhost:8888/](http://localhost:8888/). Yo
ports:
- 9999:8080
### Enabling health check
Dozzle doesn't enable healthcheck by default as it adds extra CPU usage. `healthcheck` can be enabled manually.
version: "3"
services:
dozzle:
container_name: dozzle
image: amir20/dozzle:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
ports:
- 8080:8080
environment:
DOZZLE_LEVEL: trace
healthcheck:
test: [ "CMD", "/dozzle", "healthcheck" ]
interval: 3s
timeout: 30s
retries: 5
start_period: 30s
#### Security
You can control the device Dozzle binds to by passing `--addr` parameter. For example,
@@ -105,11 +129,8 @@ Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can
| `--filter` | `DOZZLE_FILTER` | `""` |
| `--username` | `DOZZLE_USERNAME` | `""` |
| `--password` | `DOZZLE_PASSWORD` | `""` |
| `--key` | `DOZZLE_KEY` | `""` |
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
Note: When using username and password `DOZZLE_KEY` is required for session management.
## Troubleshooting and FAQs
<details>

View File

@@ -69,20 +69,6 @@ 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");
break;
default:
document.documentElement.removeAttribute("data-theme");
}
hotkeys("command+k, ctrl+k", (event, handler) => {
event.preventDefault();
showFuzzySearch();
@@ -121,7 +107,7 @@ function showFuzzySearch() {
active: true,
});
}
function onResized(e) {
function onResized(e: any) {
if (e.length == 2) {
menuWidth.value = e[0].size;
}

View File

@@ -1,8 +1,10 @@
// 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
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
CarbonCaretDown: typeof import('~icons/carbon/caret-down')['default']
@@ -11,8 +13,10 @@ declare module '@vue/runtime-core' {
ContainerStat: typeof import('./components/ContainerStat.vue')['default']
ContainerTitle: typeof import('./components/ContainerTitle.vue')['default']
DropdownMenu: typeof import('./components/DropdownMenu.vue')['default']
FieldList: typeof import('./components/FieldList.vue')['default']
FuzzySearchModal: typeof import('./components/FuzzySearchModal.vue')['default']
InfiniteLoader: typeof import('./components/InfiniteLoader.vue')['default']
JSONPayload: typeof import('./components/JSONPayload.vue')['default']
LogActionsToolbar: typeof import('./components/LogActionsToolbar.vue')['default']
LogContainer: typeof import('./components/LogContainer.vue')['default']
LogEventSource: typeof import('./components/LogEventSource.vue')['default']
@@ -38,5 +42,3 @@ declare module '@vue/runtime-core' {
SideMenu: typeof import('./components/SideMenu.vue')['default']
}
}
export {}

View File

@@ -1,34 +1,28 @@
<template>
<div class="is-size-7 is-uppercase columns is-marginless is-mobile">
<div class="is-size-7 is-uppercase columns is-marginless is-mobile" v-if="container.stat">
<div class="column is-narrow has-text-weight-bold">
{{ state }}
{{ container.state }}
</div>
<div class="column is-narrow" v-if="stat.memoryUsage !== null">
<div class="column is-narrow" v-if="container.stat.memoryUsage !== null">
<span class="has-text-weight-light has-spacer">mem</span>
<span class="has-text-weight-bold">
{{ formatBytes(stat.memoryUsage) }}
{{ formatBytes(container.stat.memoryUsage) }}
</span>
</div>
<div class="column is-narrow" v-if="stat.cpu !== null">
<div class="column is-narrow" v-if="container.stat.cpu !== null">
<span class="has-text-weight-light has-spacer">load</span>
<span class="has-text-weight-bold"> {{ stat.cpu }}% </span>
<span class="has-text-weight-bold"> {{ container.stat.cpu }}% </span>
</div>
</div>
</template>
<script lang="ts" setup>
import { ContainerStat } from "@/types/Container";
import { PropType } from "vue";
import { Container } from "@/types/Container";
import { ComputedRef, inject } from "vue";
import { formatBytes } from "@/utils";
defineProps({
stat: {
type: Object as PropType<ContainerStat>,
required: true,
},
state: String,
});
const container = inject("container") as ComputedRef<Container>;
</script>
<style lang="scss" scoped>

View File

@@ -9,13 +9,8 @@
<script lang="ts" setup>
import { Container } from "@/types/Container";
import { PropType } from "vue";
defineProps({
container: {
type: Object as PropType<Container>,
required: true,
},
});
import { inject, ComputedRef } from "vue";
const container = inject("container") as ComputedRef<Container>;
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,84 @@
<template>
<ul v-if="expanded" ref="root">
<li v-for="(value, name) in fields">
<template v-if="isObject(value)">
<span class="has-text-grey">{{ name }}=</span>
<field-list
:fields="value"
:parent-key="parentKey.concat(name)"
:visible-keys="visibleKeys"
expanded
></field-list>
</template>
<template v-else-if="Array.isArray(value)">
<a @click="toggleField(name)"> {{ hasField(name) ? "remove" : "add" }}&nbsp;</a>
<span class="has-text-grey">{{ name }}=</span>[
<span class="has-text-weight-bold" v-for="(item, index) in value">
{{ item }}
<span v-if="index !== value.length - 1">,</span>
</span>
]
</template>
<template v-else>
<a @click="toggleField(name)"> {{ hasField(name) ? "remove" : "add" }}&nbsp;</a>
<span class="has-text-grey">{{ name }}=</span><span class="has-text-weight-bold">{{ value }}</span>
</template>
</li>
</ul>
</template>
<script lang="ts" setup>
import { arrayEquals, isObject } from "@/utils";
import { nextTick, PropType, ref, toRaw } from "vue";
const props = defineProps({
fields: {
type: Object as PropType<Record<string, any>>,
required: true,
},
expanded: {
type: Boolean,
default: false,
},
parentKey: {
type: Array as PropType<string[]>,
default: [],
},
visibleKeys: {
type: Array as PropType<string[][]>,
default: [],
},
});
const root = ref<HTMLElement>();
async function toggleField(field: string) {
const index = fieldIndex(field);
if (index > -1) {
props.visibleKeys.splice(index, 1);
} else {
props.visibleKeys.push(props.parentKey.concat(field));
}
await nextTick();
root.value?.scrollIntoView({
block: "center",
});
}
function hasField(field: string) {
return fieldIndex(field) > -1;
}
function fieldIndex(field: string) {
const path = props.parentKey.concat(field);
return props.visibleKeys.findIndex((keys) => arrayEquals(toRaw(keys), toRaw(path)));
}
</script>
<style lang="scss" scoped>
ul {
margin-left: 2em;
}
</style>

View File

@@ -22,7 +22,7 @@ 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 scrollingParent = root.value?.closest("[data-scrolling]") || document.documentElement;
const previousHeight = scrollingParent.scrollHeight;
isLoading.value = true;
await props.onLoadMore();
@@ -32,7 +32,7 @@ const observer = new IntersectionObserver(async (entries) => {
}
});
onMounted(() => observer.observe(root.value));
onMounted(() => observer.observe(root.value!));
onUnmounted(() => observer.disconnect());
</script>

View File

@@ -0,0 +1,55 @@
<template>
<ul class="fields" @click="expanded = !expanded">
<li v-for="(value, name) in logEntry.message">
<template v-if="value">
<span class="has-text-grey">{{ name }}=</span>
<span class="has-text-weight-bold" v-html="markSearch(value)"></span>
</template>
</li>
</ul>
<field-list :fields="logEntry.unfilteredPayload" :expanded="expanded" :visible-keys="visibleKeys"></field-list>
</template>
<script lang="ts" setup>
import { useSearchFilter } from "@/composables/search";
import { VisibleLogEntry } from "@/types/VisibleLogEntry";
import { PropType, ref } from "vue";
const { markSearch } = useSearchFilter();
defineProps({
logEntry: {
type: Object as PropType<VisibleLogEntry>,
required: true,
},
visibleKeys: {
type: Array as PropType<string[][]>,
default: [],
},
});
const expanded = ref(false);
</script>
<style lang="scss" scoped>
.fields {
display: inline-block;
list-style: none;
&:hover {
cursor: pointer;
&::after {
content: "expand json";
color: var(--secondary-color);
display: inline-block;
margin-left: 0.5em;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
}
li {
display: inline-block;
margin-left: 1em;
}
}
</style>

View File

@@ -41,11 +41,11 @@
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, PropType } from "vue";
import { inject, onMounted, onUnmounted, PropType, ComputedRef } from "vue";
import hotkeys from "hotkeys-js";
import config from "@/stores/config";
import { Container } from "@/types/Container";
import { useSearchFilter } from "@/composables/search";
import { Container } from "@/types/Container";
const { showSearch } = useSearchFilter();
@@ -56,10 +56,6 @@ const props = defineProps({
type: Function as PropType<(e: Event) => void>,
default: (e: Event) => {},
},
container: {
type: Object as () => Container,
required: true,
},
});
const onHotkey = (event: Event) => {
@@ -67,6 +63,8 @@ const onHotkey = (event: Event) => {
event.preventDefault();
};
const container = inject("container") as ComputedRef<Container>;
onMounted(() => hotkeys("shift+command+l, shift+ctrl+l", onHotkey));
onUnmounted(() => hotkeys.unbind("shift+command+l, shift+ctrl+l", onHotkey));
</script>

View File

@@ -3,14 +3,14 @@
<template #header v-if="showTitle">
<div class="mr-0 columns is-vcentered is-marginless is-hidden-mobile">
<div class="column is-clipped is-paddingless">
<container-title :container="container" @close="$emit('close')" />
<container-title @close="$emit('close')" />
</div>
<div class="column is-narrow is-paddingless">
<container-stat :stat="container.stat" :state="container.state" v-if="container.stat" />
<container-stat v-if="container.stat" />
</div>
<div class="mr-2 column is-narrow is-paddingless">
<log-actions-toolbar :container="container" :onClearClicked="onClearClicked" />
<log-actions-toolbar :onClearClicked="onClearClicked" />
</div>
<div class="mr-2 column is-narrow is-paddingless" v-if="closable">
<button class="delete is-medium" @click="emit('close')"></button>
@@ -18,13 +18,13 @@
</div>
</template>
<template #default="{ setLoading }">
<log-viewer-with-source ref="viewer" :id="id" @loading-more="setLoading($event)" />
<log-viewer-with-source ref="viewer" @loading-more="setLoading($event)" />
</template>
</scrollable-view>
</template>
<script lang="ts" setup>
import { ref, toRefs } from "vue";
import { provide, ref, toRefs } from "vue";
import LogViewerWithSource from "./LogViewerWithSource.vue";
import { useContainerStore } from "@/stores/container";
@@ -54,6 +54,8 @@ const store = useContainerStore();
const container = store.currentContainer(id);
provide("container", container);
const viewer = ref<InstanceType<typeof LogViewerWithSource>>();
function onClearClicked() {

View File

@@ -6,29 +6,10 @@ 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 { vi, describe, expect, beforeEach, test, beforeAll, afterAll, afterEach } from "vitest";
import { computed, nextTick } from "vue";
import { createRouter, createWebHistory } from "vue-router";
vi.mock("lodash.debounce", () => ({
__esModule: true,
default: vi.fn((fn) => {
fn.cancel = () => {};
return fn;
}),
}));
vi.mock("@/stores/container", () => ({
__esModule: true,
useContainerStore() {
return {
currentContainer(id: Ref<string>) {
return computed(() => ({ id: id.value }));
},
};
},
}));
vi.mock("@/stores/config", () => ({
__esModule: true,
default: { base: "" },
@@ -47,6 +28,13 @@ describe("<LogEventSource />", () => {
observe: vi.fn(),
disconnect: vi.fn(),
}));
vi.useFakeTimers();
vi.setSystemTime(1560336942459);
});
afterEach(() => {
vi.restoreAllMocks();
vi.useRealTimers();
});
function createLogEventSource(
@@ -78,13 +66,16 @@ describe("<LogEventSource />", () => {
components: {
LogViewer,
},
provide: {
container: computed(() => ({ id: "abc", image: "test:v123" })),
},
},
slots: {
default: `
<template #scoped="params"><log-viewer :messages="params.messages"></log-viewer></template>
`,
},
props: { id: "abc" },
props: {},
});
}
@@ -111,68 +102,27 @@ describe("<LogEventSource />", () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
data: `2019-06-12T10:55:42.459034602Z "This is a message."`,
data: `{"ts":1560336942459, "m":"This is a message.", "id":1}`,
});
const [message, _] = wrapper.vm.messages;
const { key, ...messageWithoutKey } = message;
expect(key).toBe("2019-06-12T10:55:42.459034602Z");
expect(messageWithoutKey).toMatchSnapshot();
});
test("should parse messages with loki's timestamp format", async () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({ data: `2020-04-27T12:35:43.272974324+02:00 xxxxx` });
vi.runAllTimers();
await nextTick();
const [message, _] = wrapper.vm.messages;
const { key, ...messageWithoutKey } = message;
expect(key).toBe("2020-04-27T12:35:43.272974324+02:00");
expect(messageWithoutKey).toMatchSnapshot();
});
test("should pass messages to slot", async () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
data: `2019-06-12T10:55:42.459034602Z "This is a message."`,
});
const [message, _] = wrapper.getComponent(LogViewer).vm.messages;
const { key, ...messageWithoutKey } = message;
expect(key).toBe("2019-06-12T10:55:42.459034602Z");
expect(messageWithoutKey).toMatchSnapshot();
expect(message).toMatchSnapshot();
});
describe("render html correctly", () => {
const RealDate = Date;
beforeAll(() => {
// @ts-ignore
global.Date = class extends RealDate {
constructor(arg: any | number) {
super(arg);
if (arg) {
return new RealDate(arg);
} else {
return new RealDate(1560336936000);
}
}
};
});
afterAll(() => (global.Date = RealDate));
test("should render messages", async () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
data: `2019-06-12T10:55:42.459034602Z "This is a message."`,
data: `{"ts":1560336942459, "m":"This is a message.", "id":1}`,
});
await wrapper.vm.$nextTick();
vi.runAllTimers();
await nextTick();
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
});
@@ -180,10 +130,12 @@ describe("<LogEventSource />", () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
data: `2019-06-12T10:55:42.459034602Z \x1b[30mblack\x1b[37mwhite`,
data: '{"ts":1560336942459,"m":"\\u001b[30mblack\\u001b[37mwhite", "id":1}',
});
await wrapper.vm.$nextTick();
vi.runAllTimers();
await nextTick();
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
});
@@ -191,10 +143,12 @@ describe("<LogEventSource />", () => {
const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
data: `2019-06-12T10:55:42.459034602Z <test>foo bar</test>`,
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
});
await wrapper.vm.$nextTick();
vi.runAllTimers();
await nextTick();
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
});
@@ -202,10 +156,12 @@ describe("<LogEventSource />", () => {
const wrapper = createLogEventSource({ hourStyle: "12" });
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
data: `2019-06-12T23:55:42.459034602Z <test>foo bar</test>`,
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
});
await wrapper.vm.$nextTick();
vi.runAllTimers();
await nextTick();
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
});
@@ -213,10 +169,12 @@ describe("<LogEventSource />", () => {
const wrapper = createLogEventSource({ hourStyle: "24" });
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
data: `2019-06-12T23:55:42.459034602Z <test>foo bar</test>`,
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
});
await wrapper.vm.$nextTick();
vi.runAllTimers();
await nextTick();
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
});
@@ -224,13 +182,15 @@ describe("<LogEventSource />", () => {
const wrapper = createLogEventSource({ searchFilter: "test" });
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
data: `2019-06-11T10:55:42.459034602Z Foo bar`,
data: `{"ts":1560336942459, "m":"foo bar", "id":1}`,
});
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
data: `2019-06-12T10:55:42.459034602Z This is a test <hi></hi>`,
data: `{"ts":1560336942459, "m":"test bar", "id":2}`,
});
await wrapper.vm.$nextTick();
vi.runAllTimers();
await nextTick();
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
});
});

View File

@@ -1,133 +1,25 @@
<template>
<infinite-loader :onLoadMore="loadOlderLogs" :enabled="messages.length > 100"></infinite-loader>
<infinite-loader :onLoadMore="fetchMore" :enabled="messages.length > 100"></infinite-loader>
<slot :messages="messages"></slot>
</template>
<script lang="ts" setup>
import { toRefs, ref, watch, onUnmounted } from "vue";
import debounce from "lodash.debounce";
import { useEventSource } from "@/composables/eventsource";
import { Container } from "@/types/Container";
import { inject, ComputedRef } from "vue";
import { LogEntry } from "@/types/LogEntry";
import InfiniteLoader from "./InfiniteLoader.vue";
import config from "@/stores/config";
import { useContainerStore } from "@/stores/container";
const props = defineProps({
id: {
type: String,
required: true,
},
});
const { id } = toRefs(props);
const emit = defineEmits(["loading-more"]);
const store = useContainerStore();
const container = store.currentContainer(id);
const container = inject("container") as ComputedRef<Container>;
const { connect, messages, loadOlderLogs } = useEventSource(container);
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());
const beforeLoading = () => emit("loading-more", true);
const afterLoading = () => emit("loading-more", false);
defineExpose({
clear: () => (messages.value = []),
});
const fetchMore = () => loadOlderLogs({ beforeLoading, afterLoading });
connect();
</script>

View File

@@ -2,14 +2,14 @@
<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"
:key="item.id"
:data-key="item.id"
:data-event="item.event"
:class="{ selected: item.selected }"
:class="{ selected: toRaw(item) === toRaw(lastSelectedItem) }"
>
<div class="line-options" v-show="isSearching()">
<dropdown-menu :class="{ 'is-last': index === filtered.length - 1 }" class="is-top minimal">
<a class="dropdown-item" @click="handleJumpLineSelected($event, item)" :href="`#${item.key}`">
<a class="dropdown-item" @click="handleJumpLineSelected($event, item)" :href="`#${item.id}`">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
@@ -25,20 +25,27 @@
</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>
<JSONPayload :log-entry="item" :visible-keys="visibleKeys.value" v-if="item.isComplex()"></JSONPayload>
<span class="text" v-html="colorize(item.message)" v-if="item.isSimple()"></span>
</div>
</li>
</ul>
</template>
<script lang="ts" setup>
import { PropType, ref, toRefs, watch } from "vue";
import { ComputedRef, inject, PropType, ref, toRefs, watch, toRaw } 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 { VisibleLogEntry } from "@/types/VisibleLogEntry";
import { LogEntry } from "@/types/LogEntry";
import { useSearchFilter } from "@/composables/search";
import { useVisibleFilter } from "@/composables/visible";
import { Container } from "@/types/Container";
import { persistentVisibleKeys } from "@/utils";
import RelativeTime from "./RelativeTime.vue";
import AnsiConvertor from "ansi-to-html";
import JSONPayload from "./JSONPayload.vue";
const props = defineProps({
messages: {
@@ -48,18 +55,22 @@ const props = defineProps({
});
const ansiConvertor = new AnsiConvertor({ escapeXML: true });
const { filteredMessages, resetSearch, markSearch, isSearching } = useSearchFilter();
const colorize = (value: string) => markSearch(ansiConvertor.toHtml(value));
const { messages } = toRefs(props);
const filtered = filteredMessages(messages);
let visibleKeys = persistentVisibleKeys(inject("container") as ComputedRef<Container>);
const { filteredPayload } = useVisibleFilter(visibleKeys);
const { filteredMessages, resetSearch, markSearch, isSearching } = useSearchFilter();
const visible = filteredPayload(messages);
const filtered = filteredMessages(visible);
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;
let lastSelectedItem = ref<VisibleLogEntry>();
function handleJumpLineSelected(e: Event, item: VisibleLogEntry) {
lastSelectedItem.value = item;
resetSearch();
}
@@ -84,6 +95,13 @@ watch(
}
}
.text {
white-space: pre-wrap;
&::before {
content: " ";
}
}
& > li {
display: flex;
word-wrap: break-word;
@@ -95,7 +113,7 @@ watch(
&:nth-child(odd) {
background-color: rgba(125, 125, 125, 0.08);
}
&[data-event="container-stopped"] {
&[data-event="container-stopped"] {
color: #f14668;
}
&[data-event="container-started"] {
@@ -167,13 +185,6 @@ watch(
border-radius: 3px;
}
.text {
white-space: pre-wrap;
&::before {
content: " ";
}
}
:deep(mark) {
border-radius: 2px;
background-color: var(--secondary-color);

View File

@@ -1,5 +1,5 @@
<template>
<log-event-source ref="source" :id="id" #default="{ messages }" @loading-more="emit('loading-more', $event)">
<log-event-source ref="source" #default="{ messages }" @loading-more="emit('loading-more', $event)">
<log-viewer :messages="messages"></log-viewer>
</log-event-source>
</template>
@@ -7,12 +7,6 @@
<script lang="ts" setup>
import LogViewer from "./LogViewer.vue";
import { ref } from "vue";
defineProps({
id: {
type: String,
required: true,
},
});
const emit = defineEmits(["loading-more"]);

View File

@@ -16,7 +16,7 @@
<div class="is-scrollbar-notification">
<transition name="fade">
<button class="button" :class="hasMore ? 'has-more' : ''" @click="scrollToBottom('instant')" v-show="paused">
<button class="button" :class="hasMore ? 'has-more' : ''" @click="scrollToBottom()" v-show="paused">
<mdi-light-chevron-double-down />
</button>
</transition>
@@ -24,61 +24,51 @@
</section>
</template>
<script lang="ts">
export default {
props: {
scrollable: {
type: Boolean,
default: true,
},
},
<script lang="ts" setup>
import { onMounted, ref } from "vue";
name: "ScrollableView",
data() {
return {
paused: false,
hasMore: false,
loading: false,
mutationObserver: null,
intersectionObserver: null,
};
defineProps({
scrollable: {
type: Boolean,
default: true,
},
mounted() {
const { scrollableContent } = this.$refs;
this.mutationObserver = new MutationObserver((e) => {
if (!this.paused) {
this.scrollToBottom("instant");
} else {
const record = e[e.length - 1];
if (
record.target.children[record.target.children.length - 1] == record.addedNodes[record.addedNodes.length - 1]
) {
this.hasMore = true;
}
}
});
this.mutationObserver.observe(scrollableContent, { childList: true, subtree: true });
});
this.intersectionObserver = new IntersectionObserver(
(entries) => (this.paused = entries[0].intersectionRatio == 0),
{ threshholds: [0, 1], rootMargin: "80px 0px" }
);
this.intersectionObserver.observe(this.$refs.scrollObserver);
},
beforeUnmount() {
this.mutationObserver.disconnect();
this.intersectionObserver.disconnect();
},
methods: {
scrollToBottom(behavior = "instant") {
this.$refs.scrollObserver.scrollIntoView({ behavior });
this.hasMore = false;
},
setLoading(loading) {
this.loading = loading;
},
},
};
const paused = ref(false);
const hasMore = ref(false);
const loading = ref(false);
const scrollObserver = ref<HTMLElement>();
const scrollableContent = ref<HTMLElement>();
const mutationObserver = new MutationObserver((e) => {
if (!paused.value) {
scrollToBottom();
} else {
const record = e[e.length - 1];
if (record.target.children[record.target.children.length - 1] == record.addedNodes[record.addedNodes.length - 1]) {
hasMore.value = true;
}
}
});
const intersectionObserver = new IntersectionObserver((entries) => (paused.value = entries[0].intersectionRatio == 0), {
threshholds: [0, 1],
rootMargin: "80px 0px",
});
onMounted(() => {
mutationObserver.observe(scrollableContent.value!, { childList: true, subtree: true });
intersectionObserver.observe(scrollObserver.value!);
});
function scrollToBottom(behavior: "auto" | "smooth" = "auto") {
scrollObserver.value?.scrollIntoView({ behavior });
hasMore.value = false;
}
function setLoading(value: boolean) {
loading.value = value;
}
</script>
<style scoped lang="scss">
section {

View File

@@ -1,201 +1,200 @@
// 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=\\"\\">
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-cce5b553=\\"\\">
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-cce5b553=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
<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 class=\\"level-right\\" data-v-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\">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>
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span>
<!--v-if--><span class=\\"text\\" data-v-cce5b553=\\"\\">&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=\\"\\">
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-cce5b553=\\"\\">
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-cce5b553=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
<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 class=\\"level-right\\" data-v-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\">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>
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42</time></span>
<!--v-if--><span class=\\"text\\" data-v-cce5b553=\\"\\">&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=\\"\\">
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-cce5b553=\\"\\">
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-cce5b553=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
<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 class=\\"level-right\\" data-v-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\">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>
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span>
<!--v-if--><span class=\\"text\\" data-v-cce5b553=\\"\\">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=\\"\\">
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-cce5b553=\\"\\">
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-cce5b553=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
<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 class=\\"level-right\\" data-v-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\">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>
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span>
<!--v-if--><span class=\\"text\\" data-v-cce5b553=\\"\\"><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=\\"\\">
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
<li data-key=\\"2\\" class=\\"\\" data-v-cce5b553=\\"\\">
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2\\" data-v-cce5b553=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
<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 class=\\"level-right\\" data-v-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\">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>
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span>
<!--v-if--><span class=\\"text\\" data-v-cce5b553=\\"\\"><mark>test</mark> bar</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=\\"\\">
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-cce5b553=\\"\\">
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-cce5b553=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
<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 class=\\"level-right\\" data-v-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\">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>
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span>
<!--v-if--><span class=\\"text\\" data-v-cce5b553=\\"\\">&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 class=\\"infinte-loader\\" data-v-1cd63c6e=\\"\\">
<div class=\\"spinner\\" data-v-1cd63c6e=\\"\\" style=\\"display: none;\\">
<div class=\\"bounce1\\" data-v-1cd63c6e=\\"\\"></div>
<div class=\\"bounce2\\" data-v-1cd63c6e=\\"\\"></div>
<div class=\\"bounce3\\" data-v-1cd63c6e=\\"\\"></div>
</div>
</div>
<ul class=\\"events medium\\" data-v-28f125ea=\\"\\"></ul>"
<ul class=\\"events medium\\" data-v-cce5b553=\\"\\"></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.\\"",
"id": 1,
"message": "This is a message.",
}
`;

View File

@@ -0,0 +1,111 @@
import { ref, watch, onUnmounted, ComputedRef } from "vue";
import debounce from "lodash.debounce";
import type { LogEntry, LogEvent } from "@/types/LogEntry";
import config from "@/stores/config";
import { Container } from "@/types/Container";
function parseMessage(data: string): LogEntry {
const e = JSON.parse(data) as LogEvent;
const id = e.id;
const date = new Date(e.ts);
return { id, date, message: e.m };
}
export function useEventSource(container: ComputedRef<Container>) {
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=${container.value.id}&lastEventId=${lastEventId}`);
es.addEventListener("container-stopped", () => {
es?.close();
es = null;
buffer.value.push({
event: "container-stopped",
message: "Container stopped",
date: new Date(),
id: new Date().getTime(),
});
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({ beforeLoading, afterLoading } = { beforeLoading: () => {}, afterLoading: () => {} }) {
if (messages.value.length < 300) return;
beforeLoading();
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=${container.value.id}&from=${from.toISOString()}&to=${to.toISOString()}`)
).text();
if (logs) {
const newMessages = logs
.trim()
.split("\n")
.map((line) => parseMessage(line));
messages.value.unshift(...newMessages);
}
afterLoading();
}
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(),
id: new Date().getTime(),
});
connect({ clear: false });
}
}
);
onUnmounted(() => {
if (es) {
es.close();
}
});
watch(
() => container.value.id,
() => connect()
);
return { connect, messages, loadOlderLogs };
}

View File

@@ -1,21 +1,42 @@
import { ref, computed, Ref } from "vue";
import { useDebounce } from "@vueuse/core";
import { VisibleLogEntry } from "@/types/VisibleLogEntry";
const searchFilter = ref<string>("");
const debouncedSearchFilter = useDebounce(searchFilter);
const showSearch = ref(false);
import type { LogEntry } from "@/types/LogEntry";
function matchRecord(record: Record<string, any>, regex: RegExp): boolean {
for (const key in record) {
const value = record[key];
if (typeof value === "string" && regex.test(value)) {
return true;
}
if (Array.isArray(value) && matchRecord(value, regex)) {
return true;
}
}
return false;
}
export function useSearchFilter() {
const regex = computed(() => {
const isSmartCase = searchFilter.value === searchFilter.value.toLowerCase();
return isSmartCase ? new RegExp(searchFilter.value, "i") : new RegExp(searchFilter.value);
const isSmartCase = debouncedSearchFilter.value === debouncedSearchFilter.value.toLowerCase();
return isSmartCase ? new RegExp(debouncedSearchFilter.value, "i") : new RegExp(debouncedSearchFilter.value);
});
function filteredMessages(messages: Ref<LogEntry[]>) {
function filteredMessages(messages: Ref<VisibleLogEntry[]>) {
return computed(() => {
if (searchFilter && searchFilter.value) {
if (debouncedSearchFilter.value) {
try {
return messages.value.filter((d) => d.message.match(regex.value));
return messages.value.filter((d) => {
if (d.isSimple()) {
return regex.value.test(d.message);
} else if (d.isComplex()) {
return matchRecord(d.message, regex.value);
}
throw new Error("Unknown message type");
});
} catch (e) {
if (e instanceof SyntaxError) {
console.info(`Ignoring SyntaxError from search.`, e);
@@ -29,11 +50,17 @@ export function useSearchFilter() {
});
}
function markSearch(log: string) {
if (searchFilter && searchFilter.value) {
return log.replace(regex.value, `<mark>$&</mark>`);
function markSearch(log: string): string;
function markSearch(log: string[]): string[];
function markSearch(log: string | string[]) {
if (!debouncedSearchFilter.value) {
return log;
}
return log;
if (Array.isArray(log)) {
return log.map((d) => markSearch(d));
}
return log.toString().replace(regex.value, (match) => `<mark>${match}</mark>`);
}
function resetSearch() {

View File

@@ -0,0 +1,13 @@
import { LogEntry } from "@/types/LogEntry";
import { VisibleLogEntry } from "@/types/VisibleLogEntry";
import { computed, ComputedRef, Ref } from "vue";
export function useVisibleFilter(visibleKeys: ComputedRef<Ref<string[][]>>) {
function filteredPayload(messages: Ref<LogEntry[]>) {
return computed(() => {
return messages.value.map((d) => new VisibleLogEntry(d, visibleKeys.value));
});
}
return { filteredPayload };
}

View File

@@ -17,6 +17,7 @@ $menu-item-hover-color: var(--menu-item-hover-color);
$text-strong: var(--text-strong-color);
$text: var(--text-color);
$text-light: var(--text-light-color);
$panel-heading-background-color: var(--panel-heading-background-color);
$panel-heading-color: var(--panel-heading-color);
@@ -38,8 +39,7 @@ $light-toolbar-color: rgba($grey-darker, 0.7);
@import "@oruga-ui/theme-bulma/dist/scss/components/skeleton.scss";
@import "splitpanes/dist/splitpanes.css";
html,
[data-theme="dark"] {
@mixin dark {
--scheme-main: #{$black};
--scheme-main-bis: #{$black-bis};
--scheme-main-ter: #{$black-ter};
@@ -64,93 +64,57 @@ html,
--text-strong-color: #{$grey-lightest};
--text-color: #{$grey-lighter};
--text-light-color: #{$grey};
}
@mixin light {
--scheme-main: #{$white};
--scheme-main-bis: #{$white-bis};
--scheme-main-ter: #{$white-ter};
--border-color: #{$grey-lighter};
--border-hover-color: var(--secondary-color);
--logo-color: #{$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};
--text-light-color: #{$grey};
}
[data-theme="dark"] {
@include dark;
}
[data-theme="light"] {
@include light;
}
@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};
@include dark;
}
}
@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};
@include light;
}
}
[data-theme="light"] {
--scheme-main: #{$white};
--scheme-main-bis: #{$white-bis};
--scheme-main-ter: #{$white-ter};
--border-color: #{$grey-lighter};
--border-hover-color: var(--secondary-color);
--logo-color: #{$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};
}
html {
overflow-x: unset;
overflow-y: unset;

View File

@@ -4,6 +4,7 @@ export interface Container {
readonly image: string;
readonly name: string;
readonly status: string;
readonly command: string;
state: "created" | "running" | "exited" | "dead" | "paused" | "restarting";
stat?: ContainerStat;
}

View File

@@ -1,7 +1,16 @@
export interface LogEntry {
date: Date;
message: string;
key: string;
readonly date: Date;
readonly message: string | JSONObject;
readonly id: number;
event?: string;
selected?: boolean;
}
export interface LogEvent {
readonly m: string | JSONObject;
readonly ts: number;
readonly id: number;
}
export type JSONValue = string | number | boolean | JSONObject | Array<JSONValue>;
export type JSONObject = { [x: string]: JSONValue };

View File

@@ -0,0 +1,54 @@
import { computed, ComputedRef, Ref } from "vue";
import { flattenJSON, getDeep } from "@/utils";
import type { JSONObject, LogEntry } from "./LogEntry";
export class VisibleLogEntry implements LogEntry {
private readonly entry: LogEntry;
filteredMessage: undefined | ComputedRef<Record<string, any>>;
constructor(entry: LogEntry, visibleKeys: Ref<string[][]>) {
this.entry = entry;
this.filteredMessage = undefined;
if (this.isComplex()) {
const message = this.message;
this.filteredMessage = computed(() => {
if (!visibleKeys.value.length) {
return flattenJSON(message);
} else {
return visibleKeys.value.reduce((acc, attr) => ({ ...acc, [attr.join(".")]: getDeep(message, attr) }), {});
}
});
}
}
public isComplex(): this is { message: JSONObject } {
return typeof this.entry.message === "object";
}
public isSimple(): this is { message: string } {
return !this.isComplex();
}
public get unfilteredPayload(): JSONObject {
if (typeof this.entry.message === "string") {
throw new Error("Cannot get unfiltered payload of a simple message");
}
return this.entry.message;
}
public get date(): Date {
return this.entry.date;
}
public get message(): string | JSONObject {
return this.filteredMessage?.value ?? this.entry.message;
}
public get id(): number {
return this.entry.id;
}
public get event(): string | undefined {
return this.entry.event;
}
}

View File

@@ -1,3 +1,7 @@
import { Container } from "@/types/Container";
import { useStorage } from "@vueuse/core";
import { computed, ComputedRef } from "vue";
export function formatBytes(bytes: number, decimals = 2) {
if (bytes === 0) return "0 Bytes";
const k = 1024;
@@ -6,3 +10,38 @@ export function formatBytes(bytes: number, decimals = 2) {
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
}
export function getDeep(obj: Record<string, any>, path: string[]) {
return path.reduce((acc, key) => acc?.[key], obj);
}
export function isObject(value: any): value is Record<string, any> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export function flattenJSON(obj: Record<string, any>, path: string[] = []) {
const result: Record<string, any> = {};
Object.keys(obj).forEach((key) => {
const value = obj[key];
const newPath = path.concat(key);
if (isObject(value)) {
Object.assign(result, flattenJSON(value, newPath));
} else {
result[newPath.join(".")] = value;
}
});
return result;
}
export function arrayEquals(a: string[], b: string[]): boolean {
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((val, index) => val === b[index]);
}
export function persistentVisibleKeys(container: ComputedRef<Container>) {
return computed(() => useStorage(stripVersion(container.value.image) + ":" + container.value.command, []));
}
export function stripVersion(label: string) {
const [name, _] = label.split(":");
return name;
}

View File

@@ -28,6 +28,7 @@ type dockerProxy interface {
Events(context.Context, types.EventsOptions) (<-chan events.Message, <-chan error)
ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error)
ContainerStats(ctx context.Context, containerID string, stream bool) (types.ContainerStats, error)
Ping(ctx context.Context) (types.Ping, error)
}
// Client is a proxy around the docker client
@@ -38,6 +39,7 @@ type Client interface {
Events(context.Context) (<-chan ContainerEvent, <-chan error)
ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time) (io.ReadCloser, error)
ContainerStats(context.Context, string, chan<- ContainerStat) error
Ping(context.Context) (types.Ping, error)
}
// NewClientWithFilters creates a new instance of Client with docker filters
@@ -136,10 +138,15 @@ func (d *dockerClient) ContainerStats(ctx context.Context, id string, stats chan
log.Errorf("decoder for stats api returned an unknown error %v", err)
}
ncpus := uint8(v.CPUStats.OnlineCPUs)
if ncpus == 0 {
ncpus = uint8(len(v.CPUStats.CPUUsage.PercpuUsage))
}
var (
cpuDelta = float64(v.CPUStats.CPUUsage.TotalUsage) - float64(v.PreCPUStats.CPUUsage.TotalUsage)
systemDelta = float64(v.CPUStats.SystemUsage) - float64(v.PreCPUStats.SystemUsage)
cpuPercent = int64((cpuDelta / systemDelta) * float64(len(v.CPUStats.CPUUsage.PercpuUsage)) * 100)
cpuPercent = int64((cpuDelta / systemDelta) * float64(ncpus) * 100)
memUsage = int64(v.MemoryStats.Usage - v.MemoryStats.Stats["cache"])
memPercent = int64(float64(memUsage) / float64(v.MemoryStats.Limit) * 100)
)
@@ -165,6 +172,14 @@ func (d *dockerClient) ContainerStats(ctx context.Context, id string, stats chan
func (d *dockerClient) ContainerLogs(ctx context.Context, id string, tailSize int, since string) (io.ReadCloser, error) {
log.WithField("id", id).WithField("since", since).Debug("streaming logs for container")
if since != "" {
if millis, err := strconv.ParseInt(since, 10, 64); err == nil {
since = time.UnixMicro(millis).Add(time.Millisecond).Format(time.RFC3339Nano)
} else {
log.WithError(err).Debug("unable to parse since")
}
}
options := types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
@@ -241,3 +256,7 @@ func (d *dockerClient) ContainerLogsBetweenDates(ctx context.Context, id string,
return newLogReader(reader, containerJSON.Config.Tty), nil
}
func (d *dockerClient) Ping(ctx context.Context) (types.Ping, error) {
return d.cli.Ping(ctx)
}

View File

@@ -26,3 +26,9 @@ type ContainerEvent struct {
ActorID string `json:"actorId"`
Name string `json:"name"`
}
type LogEvent struct {
Message any `json:"m,omitempty"`
Timestamp int64 `json:"ts"`
Id uint32 `json:"id,omitempty"`
}

View File

@@ -1,4 +1,4 @@
FROM cypress/included:9.6.1
FROM cypress/included:10.6.0
RUN apt install curl && curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
@@ -7,5 +7,5 @@ WORKDIR /e2e
COPY pnpm-lock.yaml ./
RUN pnpm fetch
COPY package.json ./
COPY package.json tsconfig.json ./
RUN pnpm install -r --offline

13
e2e/cypress.config.ts Normal file
View File

@@ -0,0 +1,13 @@
import { defineConfig } from "cypress";
import { initPlugin } from '@frsource/cypress-plugin-visual-regression-diff/dist/plugins';
export default defineConfig({
fixturesFolder: false,
projectId: "8cua4m",
e2e: {
setupNodeEvents(on, config) {
initPlugin(on, config);
},
},
});

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -6,7 +6,7 @@ context("Dozzle default mode", { baseUrl: Cypress.env("DOZZLE_DEFAULT") }, () =>
});
it("home screen", () => {
cy.get("li.running", { timeout: 10000 }).removeDates().replaceSkippedElements().matchImageSnapshot();
cy.get("li.running", { timeout: 10000 }).removeDates().replaceSkippedElements().matchImage();
});
it("correct title", () => {

View File

@@ -0,0 +1,12 @@
/// <reference types="cypress" />
context("Dozzle dark mode", { baseUrl: Cypress.env("DOZZLE_DEFAULT") }, () => {
beforeEach(() => {
cy.visit("/");
cy.window().then((win) => win.document.documentElement.setAttribute("data-theme", "dark"));
});
it("home screen", () => {
cy.get("li.running", { timeout: 10000 }).removeDates().replaceSkippedElements().matchImage();
});
});

View File

@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@@ -1,15 +0,0 @@
/// <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

@@ -1,26 +0,0 @@
/// <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.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -1,5 +1,6 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.js shows you how to
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
@@ -23,10 +24,17 @@
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
import { addMatchImageSnapshotCommand } from "cypress-image-snapshot/command";
addMatchImageSnapshotCommand();
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }
Cypress.Commands.add("removeDates", () => {
cy.window().then((win) => win.document.querySelectorAll("time").forEach((el) => el.remove()));

View File

@@ -1,5 +1,5 @@
// ***********************************************************
// This example support/index.js is processed and
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
@@ -14,7 +14,8 @@
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "./commands";
import './commands'
import '@frsource/cypress-plugin-visual-regression-diff/dist/support';
// Alternatively you can use CommonJS syntax:
// require('./commands')

14
e2e/cypress/tsconfig.json Normal file
View File

@@ -0,0 +1,14 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": true,
// be explicit about types included
// to avoid clashing with Jest types
"types": ["cypress"]
},
"include": [
"../node_modules/cypress",
"./**/*.ts",
"./**/*.js"
]
}

View File

@@ -29,7 +29,7 @@ services:
working_dir: /e2e
volumes:
- ./cypress:/e2e/cypress
- ./cypress.json:/e2e/cypress.json
- ./cypress.config.ts:/e2e/cypress.config.ts
environment:
- CYPRESS_DOZZLE_DEFAULT=http://dozzle:8080/
- CYPRESS_CUSTOM_DEFAULT=http://custom_base:8080/foobarbase

View File

@@ -4,7 +4,8 @@
"scripts": {},
"license": "ISC",
"dependencies": {
"cypress": "^9.5.4",
"cypress-image-snapshot": "^4.0.1"
"@frsource/cypress-plugin-visual-regression-diff": "^1.9.2",
"cypress": "^10.6.0",
"typescript": "^4.7.4"
}
}

710
e2e/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

8
e2e/tsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress", "node"]
},
"include": ["**/*.ts"]
}

14
go.mod
View File

@@ -5,7 +5,7 @@ require (
github.com/alexflint/go-arg v1.4.3
github.com/beme/abide v0.0.0-20190723115211-635a09831760
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v20.10.16+incompatible
github.com/docker/docker v20.10.17+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
@@ -18,13 +18,13 @@ require (
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.8.2
github.com/stretchr/objx v0.3.0 // indirect
github.com/stretchr/testify v1.7.1
github.com/sirupsen/logrus v1.9.0
github.com/spf13/afero v1.9.2
github.com/stretchr/objx v0.4.0 // indirect
github.com/stretchr/testify v1.8.0
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
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require (

28
go.sum
View File

@@ -62,8 +62,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v20.10.16+incompatible h1:2Db6ZR/+FUR3hqPMwnogOPHFn405crbpxvWzKovETOQ=
github.com/docker/docker v20.10.16+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v20.10.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE=
github.com/docker/docker v20.10.17+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
@@ -179,21 +179,21 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo=
github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw=
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As=
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -333,8 +333,8 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -499,8 +499,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=

32
healthcheck/http.go Normal file
View File

@@ -0,0 +1,32 @@
package healthcheck
import (
"fmt"
log "github.com/sirupsen/logrus"
"net/http"
"os"
"strings"
)
func HttpRequest(addr string, base string) error {
if strings.HasPrefix(addr, ":") {
addr = "localhost" + addr
}
url := fmt.Sprintf("http://%s%s/healthcheck", addr, base)
log.Info("Checking health of " + url)
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
os.Exit(0)
}
os.Exit(1)
return nil
}

View File

@@ -1,15 +0,0 @@
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;

17
main.go
View File

@@ -15,6 +15,7 @@ import (
"github.com/alexflint/go-arg"
"github.com/amir20/dozzle/analytics"
"github.com/amir20/dozzle/docker"
"github.com/amir20/dozzle/healthcheck"
"github.com/amir20/dozzle/web"
log "github.com/sirupsen/logrus"
@@ -29,13 +30,16 @@ type args struct {
Base string `arg:"env:DOZZLE_BASE" default:"/" help:"sets the base for http router."`
Level string `arg:"env:DOZZLE_LEVEL" default:"info" help:"set Dozzle log level. Use debug for more logging."`
TailSize int `arg:"env:DOZZLE_TAILSIZE" default:"300" help:"update the initial tail size when fetching logs."`
Key string `arg:"env:DOZZLE_KEY" help:"set a random key for username and password. This is required for auth."`
Username string `arg:"env:DOZZLE_USERNAME" help:"sets the username for auth."`
Password string `arg:"env:DOZZLE_PASSWORD" help:"sets password for auth"`
NoAnalytics bool `arg:"--no-analytics,env:DOZZLE_NO_ANALYTICS" help:"disables anonymous analytics"`
WaitForDockerSeconds int `arg:"--wait-for-docker-seconds,env:DOZZLE_WAIT_FOR_DOCKER_SECONDS" help:"wait for docker to be available for at most this many seconds before starting the server."`
FilterStrings []string `arg:"env:DOZZLE_FILTER,--filter,separate" help:"filters docker containers using Docker syntax."`
Filter map[string][]string `arg:"-"`
Healthcheck *HealthcheckCmd `arg:"subcommand:healthcheck" help:"checks if the server is running."`
}
type HealthcheckCmd struct {
}
func (args) Version() string {
@@ -69,6 +73,12 @@ func main() {
DisableLevelTruncation: true,
})
if args.Healthcheck != nil {
if err := healthcheck.HttpRequest(args.Addr, args.Base); err != nil {
log.Fatal(err)
}
}
log.Infof("Dozzle version %s", version)
dockerClient := docker.NewClientWithFilters(args.Filter)
for i := 1; ; i++ {
@@ -88,10 +98,6 @@ func main() {
if args.Username == "" || args.Password == "" {
log.Fatalf("Username AND password are required for authentication")
}
if args.Key == "" {
log.Fatalf("Key is required for authentication")
}
}
config := web.Config{
@@ -99,7 +105,6 @@ func main() {
Base: args.Base,
Version: version,
TailSize: args.TailSize,
Key: args.Key,
Username: args.Username,
Password: args.Password,
}

View File

@@ -1,6 +1,6 @@
{
"name": "dozzle",
"version": "3.12.6",
"version": "4.0.1",
"description": "Realtime log viewer for docker containers. ",
"homepage": "https://github.com/amir20/dozzle#readme",
"bugs": {
@@ -22,53 +22,54 @@
"postinstall": "husky install"
},
"dependencies": {
"@iconify-json/carbon": "^1.1.4",
"@iconify-json/cil": "^1.1.1",
"@iconify-json/mdi": "^1.1.12",
"@iconify-json/mdi-light": "^1.1.1",
"@iconify-json/octicon": "^1.1.7",
"@iconify-json/carbon": "^1.1.7",
"@iconify-json/cil": "^1.1.2",
"@iconify-json/mdi": "^1.1.30",
"@iconify-json/mdi-light": "^1.1.2",
"@iconify-json/octicon": "^1.1.16",
"@oruga-ui/oruga-next": "^0.5.4",
"@oruga-ui/theme-bulma": "^0.2.5",
"@vitejs/plugin-vue": "^2.3.3",
"@vue/compiler-sfc": "^3.2.33",
"@vueuse/core": "^8.4.2",
"@vueuse/router": "^8.4.2",
"@oruga-ui/theme-bulma": "^0.2.6",
"@vitejs/plugin-vue": "3.0.3",
"@vue/compiler-sfc": "^3.2.37",
"@vueuse/core": "^9.1.0",
"@vueuse/router": "^9.1.0",
"ansi-to-html": "^0.7.2",
"bulma": "^0.9.4",
"date-fns": "^2.28.0",
"fuzzysort": "^1.9.0",
"hotkeys-js": "^3.9.3",
"date-fns": "^2.29.2",
"fuzzysort": "^2.0.1",
"hotkeys-js": "^3.9.4",
"lodash.debounce": "^4.0.8",
"pinia": "^2.0.14",
"sass": "^1.51.0",
"pinia": "^2.0.20",
"sass": "^1.54.4",
"semver": "^7.3.7",
"splitpanes": "^3.1.1",
"typescript": "^4.6.4",
"unplugin-auto-import": "^0.7.1",
"unplugin-icons": "^0.14.3",
"unplugin-vue-components": "^0.19.5",
"vite": "^2.9.9",
"vue": "^3.2.33",
"vue-router": "^4.0.15"
"typescript": "^4.7.4",
"unplugin-auto-import": "^0.11.2",
"unplugin-icons": "^0.14.8",
"unplugin-vue-components": "^0.22.4",
"vite": "3.0.9",
"vue": "^3.2.37",
"vue-router": "^4.1.3"
},
"devDependencies": {
"@pinia/testing": "^0.0.12",
"@types/jest": "^27.5.1",
"@pinia/testing": "^0.0.14",
"@types/jest": "^28.1.7",
"@types/lodash.debounce": "^4.0.7",
"@types/node": "^17.0.32",
"@types/semver": "^7.3.9",
"@vue/test-utils": "^2.0.0-rc.21",
"c8": "^7.11.2",
"@types/node": "^18.7.7",
"@types/semver": "^7.3.12",
"@vue/test-utils": "^2.0.2",
"c8": "^7.12.0",
"eventsourcemock": "^2.0.0",
"husky": "^8.0.1",
"jest-serializer-vue": "^2.0.2",
"jsdom": "^19.0.0",
"lint-staged": "^12.4.1",
"jsdom": "^20.0.0",
"lint-staged": "^13.0.3",
"npm-run-all": "^4.1.5",
"prettier": "^2.6.2",
"release-it": "^15.0.0",
"ts-node": "^10.6.0",
"vitest": "^0.12.4"
"prettier": "^2.7.1",
"release-it": "^15.3.0",
"ts-node": "^10.9.1",
"vitest": "^0.22.1",
"vue-tsc": "^0.40.1"
},
"lint-staged": {
"*.{js,vue,css}": [

2490
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,8 @@
"forceConsistentCasingInFileNames": true,
"paths": {
"@/*": ["assets/*"]
}
},
"jsx": "preserve",
},
"include": ["assets/**/*.ts", "assets/**/*.d.ts", "assets/**/*.vue"],

View File

@@ -144,7 +144,7 @@ Connection: keep-alive
Content-Type: text/event-stream
X-Accel-Buffering: no
data: INFO Testing logs...
data: {"m":"INFO Testing logs...","ts":0,"id":4256192898}
event: container-stopped
data: end of stream
@@ -170,8 +170,8 @@ Connection: keep-alive
Content-Type: text/event-stream
X-Accel-Buffering: no
data: 2020-05-13T18:55:37.772853839Z INFO Testing logs...
id: 2020-05-13T18:55:37.772853839Z
data: {"m":"INFO Testing logs...","ts":1589396137772,"id":1469707724}
id: 1589396137772
event: container-stopped
data: end of stream

View File

@@ -1,6 +1,8 @@
package web
import (
"crypto/sha256"
"fmt"
"net/http"
"time"
@@ -17,7 +19,7 @@ const sessionName = "session"
func initializeAuth(h *handler) {
secured = false
if h.config.Username != "" && h.config.Password != "" {
store = sessions.NewCookieStore([]byte(h.config.Key))
store = sessions.NewCookieStore(generateSessionStorageKey(h.config.Username, h.config.Password))
store.Options.HttpOnly = true
store.Options.SameSite = http.SameSiteLaxMode
store.Options.MaxAge = 0
@@ -35,7 +37,7 @@ func authorizationRequired(f http.HandlerFunc) http.Handler {
}
})
} else {
return http.HandlerFunc(f)
return f
}
}
@@ -115,3 +117,8 @@ func (h *handler) clearSession(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, h.config.Base, http.StatusTemporaryRedirect)
}
func generateSessionStorageKey(username string, password string) []byte {
key := sha256.Sum256([]byte(fmt.Sprintf("%s:%s", username, password)))
return key[:]
}

View File

@@ -4,6 +4,8 @@ import (
"bufio"
"compress/gzip"
"context"
"encoding/json"
"hash/fnv"
"fmt"
"io"
@@ -13,29 +15,12 @@ import (
"time"
"github.com/amir20/dozzle/docker"
"github.com/dustin/go-humanize"
log "github.com/sirupsen/logrus"
)
func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
from, _ := time.Parse(time.RFC3339, r.URL.Query().Get("from"))
to, _ := time.Parse(time.RFC3339, r.URL.Query().Get("to"))
id := r.URL.Query().Get("id")
reader, err := h.client.ContainerLogsBetweenDates(r.Context(), id, from, to)
defer reader.Close()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
io.Copy(w, reader)
}
func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
container, err := h.client.FindContainer(id)
@@ -63,6 +48,64 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
io.Copy(zw, reader)
}
func logEventIterator(reader *bufio.Reader) func() (docker.LogEvent, error) {
return func() (docker.LogEvent, error) {
message, readerError := reader.ReadString('\n')
h := fnv.New32a()
h.Write([]byte(message))
logEvent := docker.LogEvent{Id: h.Sum32(), Message: message}
if index := strings.IndexAny(message, " "); index != -1 {
logId := message[:index]
if timestamp, err := time.Parse(time.RFC3339Nano, logId); err == nil {
logEvent.Timestamp = timestamp.UnixMilli()
message = strings.TrimSuffix(message[index+1:], "\n")
logEvent.Message = message
if strings.HasPrefix(message, "{") && strings.HasSuffix(message, "}") {
var data map[string]interface{}
if err := json.Unmarshal([]byte(message), &data); err != nil {
log.Errorf("json unmarshal error while streaming %v", err.Error())
}
logEvent.Message = data
}
}
}
return logEvent, readerError
}
}
func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/ld+json; charset=UTF-8")
from, _ := time.Parse(time.RFC3339, r.URL.Query().Get("from"))
to, _ := time.Parse(time.RFC3339, r.URL.Query().Get("to"))
id := r.URL.Query().Get("id")
reader, err := h.client.ContainerLogsBetweenDates(r.Context(), id, from, to)
defer reader.Close()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
buffered := bufio.NewReader(reader)
eventIterator := logEventIterator(buffered)
for {
logEvent, readerError := eventIterator()
if readerError != nil {
break
}
if err := json.NewEncoder(w).Encode(logEvent); err != nil {
log.Errorf("json encoding error while streaming %v", err.Error())
}
}
}
func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
if id == "" {
@@ -105,17 +148,36 @@ func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
}
defer reader.Close()
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
go func() {
for {
select {
case <-r.Context().Done():
return
case <-ticker.C:
fmt.Fprintf(w, ":ping \n\n")
f.Flush()
}
}
}()
buffered := bufio.NewReader(reader)
var readerError error
var message string
eventIterator := logEventIterator(buffered)
for {
message, readerError = buffered.ReadString('\n')
fmt.Fprintf(w, "data: %s\n", message)
if index := strings.IndexAny(message, " "); index != -1 {
id := message[:index]
if _, err := time.Parse(time.RFC3339Nano, id); err == nil {
fmt.Fprintf(w, "id: %s\n", id)
}
var logEvent docker.LogEvent
logEvent, readerError = eventIterator()
if buf, err := json.Marshal(logEvent); err != nil {
log.Errorf("json encoding error while streaming %v", err.Error())
} else {
fmt.Fprintf(w, "data: %s\n", buf)
}
if logEvent.Timestamp > 0 {
fmt.Fprintf(w, "id: %d\n", logEvent.Timestamp)
}
fmt.Fprintf(w, "\n")
f.Flush()

View File

@@ -20,7 +20,6 @@ type Config struct {
Addr string
Version string
TailSize int
Key string
Username string
Password string
}
@@ -50,9 +49,9 @@ func createRouter(h *handler) *mux.Router {
r := mux.NewRouter()
r.Use(cspHeaders)
if base != "/" {
r.HandleFunc(base, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
r.HandleFunc(base, func(w http.ResponseWriter, req *http.Request) {
http.Redirect(w, req, base+"/", http.StatusMovedPermanently)
}))
})
}
s := r.PathPrefix(base).Subrouter()
s.Handle("/api/logs/stream", authorizationRequired(h.streamLogs))
@@ -62,6 +61,7 @@ func createRouter(h *handler) *mux.Router {
s.HandleFunc("/api/validateCredentials", h.validateCredentials)
s.Handle("/logout", authorizationRequired(h.clearSession))
s.Handle("/version", authorizationRequired(h.version))
s.HandleFunc("/healthcheck", h.healthcheck)
if log.IsLevelEnabled(log.DebugLevel) {
s.PathPrefix("/debug/pprof/").Handler(http.DefaultServeMux)
@@ -132,3 +132,14 @@ func (h *handler) version(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "text/html")
fmt.Fprintf(w, "<pre>%v</pre>", h.config.Version)
}
func (h *handler) healthcheck(w http.ResponseWriter, r *http.Request) {
log.Trace("Executing healthcheck request")
if ping, err := h.client.Ping(r.Context()); err != nil {
log.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
} else {
fmt.Fprintf(w, "OK API Version %v", ping.APIVersion)
}
}

View File

@@ -271,7 +271,7 @@ func Test_createRoutes_redirect_with_auth(t *testing.T) {
fs := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fs, "index.html", []byte("index page"), 0644), "WriteFile should have no error.")
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/foobar", Username: "amir", Password: "password", Key: "key"})
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/foobar", Username: "amir", Password: "password"})
req, err := http.NewRequest("GET", "/foobar/", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
@@ -320,7 +320,7 @@ func Test_createRoutes_version(t *testing.T) {
func Test_createRoutes_username_password(t *testing.T) {
handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password", Key: "key"})
handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password"})
req, err := http.NewRequest("GET", "/", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
@@ -329,7 +329,7 @@ func Test_createRoutes_username_password(t *testing.T) {
}
func Test_createRoutes_username_password_invalid(t *testing.T) {
handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password", Key: "key"})
handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password"})
req, err := http.NewRequest("GET", "/api/logs/stream?id=123", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
@@ -338,7 +338,7 @@ func Test_createRoutes_username_password_invalid(t *testing.T) {
}
func Test_createRoutes_username_password_login_happy(t *testing.T) {
handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password", Key: "key"})
handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password"})
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
@@ -368,7 +368,7 @@ func Test_createRoutes_username_password_login_happy(t *testing.T) {
}
func Test_createRoutes_username_password_login_failed(t *testing.T) {
handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password", Key: "key"})
handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password"})
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
@@ -398,7 +398,7 @@ func Test_createRoutes_username_password_valid_session(t *testing.T) {
mockedClient := new(MockedClient)
mockedClient.On("FindContainer", "123").Return(docker.Container{ID: "123"}, nil)
mockedClient.On("ContainerLogs", mock.Anything, "123", 0).Return(ioutil.NopCloser(strings.NewReader("test data")), io.EOF)
handler := createHandler(mockedClient, nil, Config{Base: "/", Username: "amir", Password: "password", Key: "key"})
handler := createHandler(mockedClient, nil, Config{Base: "/", Username: "amir", Password: "password"})
// Get cookie first
req, err := http.NewRequest("GET", "/api/logs/stream?id=123", nil)
@@ -422,7 +422,7 @@ func Test_createRoutes_username_password_invalid_session(t *testing.T) {
mockedClient := new(MockedClient)
mockedClient.On("FindContainer", "123").Return(docker.Container{ID: "123"}, nil)
mockedClient.On("ContainerLogs", mock.Anything, "123", 0).Return(ioutil.NopCloser(strings.NewReader("test data")), io.EOF)
handler := createHandler(mockedClient, nil, Config{Base: "/", Username: "amir", Password: "password", Key: "key"})
handler := createHandler(mockedClient, nil, Config{Base: "/", Username: "amir", Password: "password"})
req, err := http.NewRequest("GET", "/api/logs/stream?id=123", nil)
require.NoError(t, err, "NewRequest should not return an error.")
req.AddCookie(&http.Cookie{Name: "session", Value: "baddata"})