Compare commits

...

209 Commits

Author SHA1 Message Date
IrwenXYZ
ae3283adda Update readme for memory consumption on ARM devices (#1967) 2022-11-25 20:19:39 -08:00
renovate[bot]
145158a925 Update dependency unplugin-auto-import to ^0.12.0 (#1966) 2022-11-25 09:56:19 -08:00
renovate[bot]
e46e1a0814 Update cypress/included Docker tag to v11.2.0 (#1961) 2022-11-23 06:34:10 -08:00
renovate[bot]
7a624cd065 Update dependency jest-serializer-vue to v3 (#1959) 2022-11-23 06:33:56 -08:00
Amir Raminfar
fe7c8743b9 Removes depedenbot 2022-11-15 15:40:58 -08:00
renovate[bot]
ccc1aae78e Update cypress/included Docker tag to v11.1.0 (#1958)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-15 13:05:01 -08:00
renovate[bot]
935e17c200 Update dependency vite to v3.2.4 (#1957) 2022-11-15 07:11:03 -08:00
renovate[bot]
63bb1f24a3 Update module github.com/spf13/afero to v1.9.3 (#1954) 2022-11-14 20:11:08 -08:00
renovate[bot]
cdc8189ad1 Update module go to 1.19 (#1955) 2022-11-14 20:09:59 -08:00
renovate[bot]
f6daab2111 Add renovate.json (#1953) 2022-11-14 19:43:38 -08:00
Amir Raminfar
7ed461e6ef Removes npmrc and hositing is not needed anymore (#1952) 2022-11-14 09:42:01 -08:00
Amir Raminfar
15636348b5 Updates modules 2022-11-10 09:35:11 -08:00
kodiakhq[bot]
a370ad48d5 Merge pull request #1950 from amir20/dependabot/docker/e2e/cypress/included-11.0.1
Bump cypress/included from 11.0.0 to 11.0.1 in /e2e
2022-11-10 09:11:52 +00:00
dependabot[bot]
8900631b20 Bump cypress/included from 11.0.0 to 11.0.1 in /e2e
Bumps cypress/included from 11.0.0 to 11.0.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-11-10 09:06:19 +00:00
Amir Raminfar
fa3ef24999 Updates cypress to 11.x (#1949) 2022-11-09 09:11:02 -08:00
kodiakhq[bot]
deaf58c47b Merge pull request #1948 from amir20/dependabot/docker/e2e/cypress/included-11.0.0
Bump cypress/included from 10.11.0 to 11.0.0 in /e2e
2022-11-09 09:11:24 +00:00
dependabot[bot]
1d4573a657 Bump cypress/included from 10.11.0 to 11.0.0 in /e2e
Bumps cypress/included from 10.11.0 to 11.0.0.

---
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-11-09 09:07:24 +00:00
Amir Raminfar
84300fc734 Release 4.4.1 2022-11-08 15:10:14 -08:00
Dmitry Mazurov
2b9873e76b Update mobile menu (#1946) 2022-11-08 15:09:57 -08:00
Amir Raminfar
c589c599fc Release 4.4.0 2022-11-08 12:48:48 -08:00
Amir Raminfar
2702e80314 Adds hover for buttons 2022-11-08 12:47:10 -08:00
Dmitry Mazurov
bb1dda41ff Add save collapse to localstorage (#1945) 2022-11-08 12:40:13 -08:00
Amir Raminfar
ddafd95b69 Fixes int tests 2022-11-08 11:54:37 -08:00
Amir Raminfar
d6e87a7137 Fixes collapseNav 2022-11-08 11:49:40 -08:00
Dmitry Mazurov
54eca89c4e Moved logout button to sidebar and fix default width sidebar. (#1942)
* Moved logout button to sidebar and fix default width sidebar.

Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>

* Fix logout url. Save collapse to localstorage.

Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>

* Clear code.

Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>

* Update logout button if collapse

Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>

* Fix dark mode for collapse button. Update toolbar. Remove localstorage.

* Fix collapse auto-import

* Remove collapse from settings

Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>
2022-11-08 11:45:11 -08:00
Amir Raminfar
fbe34d7377 Updates modules 2022-11-07 09:08:29 -08:00
Dmitry Mazurov
e2b497f158 Fix search modal for mobile view (#1941)
Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>

Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>
2022-11-04 10:36:19 -07:00
Amir Raminfar
ca9fcf8e1d Updates snapshots 2022-11-03 13:43:24 -07:00
Dmitry Mazurov
4fb012c0e4 Merge branch 'amir20:master' into responsible 2022-11-03 22:48:44 +03:00
Dmitry Mazurov
b77448f2a9 Fix responsible list containers
Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>
2022-11-03 22:09:56 +03:00
Amir Raminfar
3bc921c8d7 Updates auto import 2022-11-03 09:51:17 -07:00
Amir Raminfar
54c08cf79b Removes duplicate case sensistive files (#1939) 2022-11-03 09:41:59 -07:00
Amir Raminfar
199c51722c Also skip docker login 2022-11-02 10:43:08 -07:00
Amir Raminfar
379af164dd Skips only the push int tests (#1937) 2022-11-02 09:16:45 -07:00
Dmitry Mazurov
dc4ba652b4 Skip Integration Tests and Push branches and PRs from fork (#1936)
Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>

Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>
2022-11-02 07:24:39 -07:00
kodiakhq[bot]
0e8561a1c5 Merge pull request #1935 from amir20/dependabot/docker/golang-1.19.3-alpine
Bump golang from 1.19.2-alpine to 1.19.3-alpine
2022-11-02 09:29:57 +00:00
dependabot[bot]
60ec928a8e Bump golang from 1.19.2-alpine to 1.19.3-alpine
Bumps golang from 1.19.2-alpine to 1.19.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-11-02 09:26:23 +00:00
Dmitry Mazurov
26f3ab65a7 Update ru locale (#1934) 2022-11-01 11:36:02 -07:00
Amir Raminfar
0cc209484d Use trim instead 2022-11-01 10:57:49 -07:00
Amir Raminfar
f09a7815af Cleans up DockerSecret 2022-11-01 09:08:05 -07:00
Amir Raminfar
70b573cec0 Fixes spacing 2022-10-31 15:11:33 -07:00
Amir Raminfar
0d79196eda Updates cypress 2022-10-31 10:21:41 -07:00
Amir Raminfar
8eb4e05055 Release 4.3.0 2022-10-31 09:33:26 -07:00
Amir Raminfar
cd793a9b53 Updates modules 2022-10-31 09:32:21 -07:00
Dmitry Mazurov
fad75e2f5b Fix read username and password from file (#1930)
* Fix read username and password from file

* Update Readme

* Add ru locale

* Update read the file from UnmarshalText

* Fix if not set username and password

* Fix if content in file empty

* Update ru locale

* Fix errorMsg for auth

* Removed local ru

Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>

* Remove struct for username and password

Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>
2022-10-31 09:31:27 -07:00
Dmitry Mazurov
d6710453e5 Add ru locale (#1932)
Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>

Signed-off-by: Dmitry Mazurov <dimabzz@gmail.com>
2022-10-31 09:03:47 -07:00
Amir Raminfar
3a8c9cbcfa Release 4.2.3 2022-10-28 14:42:46 -07:00
Dmitry Mazurov
8586fee5ab Add docker secrets env (#1929) 2022-10-28 14:31:48 -07:00
Amir Raminfar
8ccae473ed Updates modules 2022-10-28 12:24:42 -07:00
kodiakhq[bot]
02434afef3 Merge pull request #1923 from amir20/dependabot/docker/e2e/cypress/included-10.11.0
Bump cypress/included from 10.10.0 to 10.11.0 in /e2e
2022-10-26 09:45:31 +00:00
dependabot[bot]
192ee98dd1 Bump cypress/included from 10.10.0 to 10.11.0 in /e2e
Bumps cypress/included from 10.10.0 to 10.11.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-10-26 09:41:20 +00:00
kodiakhq[bot]
79e9dc7be6 Merge pull request #1922 from amir20/dependabot/go_modules/github.com/docker/docker-20.10.21incompatible
Bump github.com/docker/docker from 20.10.20+incompatible to 20.10.21+incompatible
2022-10-26 09:15:01 +00:00
dependabot[bot]
19fe7e8d18 Bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 20.10.20+incompatible to 20.10.21+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.20...v20.10.21)

---
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-10-26 09:11:08 +00:00
Amir Raminfar
e4ecfca5be Updats modules 2022-10-24 16:08:59 -07:00
kodiakhq[bot]
f37925b637 Merge pull request #1920 from amir20/dependabot/go_modules/github.com/stretchr/testify-1.8.1
Bump github.com/stretchr/testify from 1.8.0 to 1.8.1
2022-10-24 09:15:06 +00:00
dependabot[bot]
822b783063 Bump github.com/stretchr/testify from 1.8.0 to 1.8.1
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.0...v1.8.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-24 09:11:05 +00:00
Amir Raminfar
7dcf1d4f15 Release 4.2.2 2022-10-21 13:14:07 -07:00
Amir Raminfar
d0af303d6f Updates colors (#1919)
* Updates colors

* Updates colors int tests
2022-10-21 13:13:48 -07:00
Amir Raminfar
fd31d394a5 Fixes tooltips 2022-10-19 15:16:42 -07:00
Amir Raminfar
1c0af19c88 Release 4.2.1 2022-10-19 08:31:26 -07:00
Amir Raminfar
9d77613ee9 Updates node 2022-10-19 08:31:13 -07:00
kodiakhq[bot]
62f747797c Merge pull request #1917 from amir20/dependabot/go_modules/github.com/docker/docker-20.10.20incompatible
Bump github.com/docker/docker from 20.10.19+incompatible to 20.10.20+incompatible
2022-10-19 09:18:28 +00:00
kodiakhq[bot]
fccb7fc2d4 Merge pull request #1916 from amir20/dependabot/github_actions/docker/setup-buildx-action-2.2.1
Bump docker/setup-buildx-action from 2.2.0 to 2.2.1
2022-10-19 09:17:37 +00:00
kodiakhq[bot]
7fecbdd000 Merge pull request #1915 from amir20/dependabot/docker/node-19-alpine
Bump node from 18-alpine to 19-alpine
2022-10-19 09:17:24 +00:00
dependabot[bot]
46a2f2b810 Bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 20.10.19+incompatible to 20.10.20+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.19...v20.10.20)

---
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-10-19 09:12:59 +00:00
dependabot[bot]
e9dcdda64d Bump docker/setup-buildx-action from 2.2.0 to 2.2.1
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.2.0 to 2.2.1.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2.2.0...v2.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-19 09:12:50 +00:00
dependabot[bot]
67a0644c37 Bump node from 18-alpine to 19-alpine
Bumps node from 18-alpine to 19-alpine.

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-19 09:12:48 +00:00
Amir Raminfar
21fc2ce2fd Adds on mouse over effect for stats (#1914) 2022-10-18 15:29:19 -07:00
kodiakhq[bot]
20425bf6b1 Merge pull request #1913 from amir20/dependabot/github_actions/docker/setup-buildx-action-2.2.0
Bump docker/setup-buildx-action from 2.1.0 to 2.2.0
2022-10-18 09:17:58 +00:00
dependabot[bot]
828c288570 Bump docker/setup-buildx-action from 2.1.0 to 2.2.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.1.0 to 2.2.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2.1.0...v2.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-18 09:15:02 +00:00
Amir Raminfar
cde2589755 Fixes iOS font size changing 2022-10-17 10:48:11 -07:00
kodiakhq[bot]
5fb2f452e2 Merge pull request #1911 from amir20/dependabot/github_actions/pnpm/action-setup-2.2.4
Bump pnpm/action-setup from 2.2.3 to 2.2.4
2022-10-17 09:16:40 +00:00
dependabot[bot]
6aea252d3e Bump pnpm/action-setup from 2.2.3 to 2.2.4
Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 2.2.3 to 2.2.4.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v2.2.3...v2.2.4)

---
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-10-17 09:13:39 +00:00
Amir Raminfar
7337dcb5d4 Updates modules 2022-10-14 14:57:46 -07:00
Amir Raminfar
deeb5fc100 Release 4.2.0 2022-10-14 13:23:59 -07:00
Amir Raminfar
5b15a0b29d stat cleanup (#1910)
* Updates modules

* Cleans up stats
2022-10-14 13:00:57 -07:00
kodiakhq[bot]
e2ad2e0193 Merge pull request #1909 from amir20/dependabot/go_modules/github.com/docker/docker-20.10.19incompatible
Bump github.com/docker/docker from 20.10.18+incompatible to 20.10.19+incompatible
2022-10-14 09:11:40 +00:00
dependabot[bot]
5b48426fc1 Bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 20.10.18+incompatible to 20.10.19+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.18...v20.10.19)

---
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-10-14 09:08:05 +00:00
kodiakhq[bot]
a24947ab3b Merge pull request #1908 from amir20/dependabot/github_actions/docker/setup-buildx-action-2.1.0
Bump docker/setup-buildx-action from 2.0.0 to 2.1.0
2022-10-13 09:19:44 +00:00
dependabot[bot]
8926b451d0 Bump docker/setup-buildx-action from 2.0.0 to 2.1.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.0.0 to 2.1.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2.0.0...v2.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-13 09:16:51 +00:00
Amir Raminfar
afd37d3455 Add trending CPU and Memory usage (#1896)
* Refactors to stat history

* Uses markRaw

* Cleans up proxies

* Removes id from snapshots

* Adds d3

* Adds more d3 modules

* Fixes package

* Cleans up packages

* Updates modules

* Adds initital d3 chart

* Cleans up svg

* Fixes @types/d3-array

* Adds memory

* Moves charts around
2022-10-12 14:21:41 -07:00
kodiakhq[bot]
4f84beb835 Merge pull request #1906 from amir20/dependabot/docker/e2e/cypress/included-10.10.0
Bump cypress/included from 10.9.0 to 10.10.0 in /e2e
2022-10-12 10:43:39 +00:00
dependabot[bot]
d54d894a66 Bump cypress/included from 10.9.0 to 10.10.0 in /e2e
Bumps cypress/included from 10.9.0 to 10.10.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-10-12 10:39:36 +00:00
kodiakhq[bot]
7c854d31a7 Merge pull request #1905 from amir20/dependabot/github_actions/docker/login-action-2.1.0
Bump docker/login-action from 2.0.0 to 2.1.0
2022-10-12 09:33:27 +00:00
kodiakhq[bot]
4c2caad4a0 Merge pull request #1904 from amir20/dependabot/github_actions/docker/build-push-action-3.2.0
Bump docker/build-push-action from 3.1.1 to 3.2.0
2022-10-12 09:33:14 +00:00
dependabot[bot]
9684fd978b Bump docker/login-action from 2.0.0 to 2.1.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 2.0.0 to 2.1.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2.0.0...v2.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-12 09:29:20 +00:00
dependabot[bot]
a79a3f680f Bump docker/build-push-action from 3.1.1 to 3.2.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3.1.1 to 3.2.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v3.1.1...v3.2.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-10-12 09:29:17 +00:00
kodiakhq[bot]
1e18423b04 Merge pull request #1903 from amir20/dependabot/github_actions/pnpm/action-setup-2.2.3
Bump pnpm/action-setup from 2.2.2 to 2.2.3
2022-10-11 09:14:13 +00:00
dependabot[bot]
82b208c0bf Bump pnpm/action-setup from 2.2.2 to 2.2.3
Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 2.2.2 to 2.2.3.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v2.2.2...v2.2.3)

---
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-10-11 09:08:59 +00:00
Amir Raminfar
8a3e9504a4 Updates modules 2022-10-10 15:38:05 -07:00
Amir Raminfar
d8748c6e27 Updats modules 2022-10-08 13:26:47 -07:00
kodiakhq[bot]
af8192fbc0 Merge pull request #1897 from amir20/dependabot/docker/golang-1.19.2-alpine
Bump golang from 1.19.1-alpine to 1.19.2-alpine
2022-10-05 09:20:23 +00:00
dependabot[bot]
3492587d63 Bump golang from 1.19.1-alpine to 1.19.2-alpine
Bumps golang from 1.19.1-alpine to 1.19.2-alpine.

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-05 09:16:19 +00:00
Amir Raminfar
b8c522021d Updates modules 2022-10-04 13:26:11 -07:00
James Cote
5850c319f2 Fix json log line still says "expand json" when expanded (#1894) 2022-10-01 15:06:59 -07:00
Amir Raminfar
afbed43185 Release 4.1.9 2022-09-28 12:29:26 -07:00
Altynbek Kaliakbarov
d083430c73 Change calculation of memory usage. (#1892) 2022-09-28 12:23:24 -07:00
kodiakhq[bot]
79f553ff0c Merge pull request #1890 from amir20/dependabot/docker/e2e/cypress/included-10.9.0
Bump cypress/included from 10.8.0 to 10.9.0 in /e2e
2022-09-28 09:38:56 +00:00
dependabot[bot]
a02551f5ec Bump cypress/included from 10.8.0 to 10.9.0 in /e2e
Bumps cypress/included from 10.8.0 to 10.9.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-09-28 09:33:10 +00:00
Amir Raminfar
7c486d57fc Release 4.1.8 2022-09-27 10:27:57 -07:00
Amir Raminfar
41acf28be9 Updates modules 2022-09-26 09:54:25 -07:00
Amir Raminfar
21e5f4fc56 Removes fuzzysearch and uses fuse.js (#1888)
* Removes fuzzysearch and uses fuse.js

* Fix search bug

* Adds locale

* Fixes more locale

* Fixes search to not update with stats

* Fixes sort order
2022-09-23 09:54:55 -07:00
kodiakhq[bot]
2d54cbba9c Merge pull request #1887 from amir20/keyboard
Improves keyboard short cuts and adds a test
2022-09-22 18:19:41 +00:00
Amir Raminfar
88844c895c Fixes comment 2022-09-22 11:15:43 -07:00
Amir Raminfar
7aa7f42c52 Improves keyboard short cuts and adds a test 2022-09-22 11:13:28 -07:00
Amir Raminfar
59f4b0da4f Release 4.1.7 2022-09-20 14:31:07 -07:00
Amir Raminfar
e99e6ebd49 Fixes bubble for search and #1886 2022-09-20 14:30:54 -07:00
Amir Raminfar
3268c32627 Release 4.1.6 2022-09-20 14:03:27 -07:00
Amir Raminfar
5cb5cae113 Fixes spacing bug 2022-09-20 14:03:20 -07:00
Amir Raminfar
dbd1050948 Release 4.1.5 2022-09-20 13:15:04 -07:00
Amir Raminfar
69c647336e Cleans up log viewer and adds zigzag line for skipped logs (#1885)
* Cleans up log viewer and adds zigzag line for skipped logs

* Updates components

* Cleans up css

* Cleans up more css

* Fixes tests

* Fixes typing

* Fixes typescript errors

* Fixes selected color
2022-09-20 13:14:31 -07:00
Amir Raminfar
1a1dd74142 Updates snapshots 2022-09-19 10:32:35 -07:00
Amir Raminfar
307dcd1929 Updates modules 2022-09-19 09:57:16 -07:00
Amir Raminfar
06bde85e03 Removes hotkeyjs in favor of vueuse (#1883) 2022-09-16 09:09:21 -07:00
Amir Raminfar
9cbb55d780 Updates tests 2022-09-16 08:37:03 -07:00
Amir Raminfar
744bc11a2e Release 4.1.4 2022-09-15 15:29:32 -07:00
Amir Raminfar
4fe8964d66 Fixes search for test 2022-09-15 15:29:26 -07:00
Amir Raminfar
3f13ef28a9 Release 4.1.3 2022-09-15 15:21:32 -07:00
Amir Raminfar
28c569ce2a Fixes find in context bug 2022-09-15 15:21:15 -07:00
Amir Raminfar
c1dd3c1131 Search is broken with unknow types. See #1881 2022-09-15 11:16:21 -07:00
Amir Raminfar
492706367b Release 4.1.2 2022-09-15 09:26:34 -07:00
Amir Raminfar
cbdbe65b7b Fixes scrollback bug where messages weren't being dropped 2022-09-15 09:26:21 -07:00
Amir Raminfar
de84736ba4 Updates modules 2022-09-15 09:13:26 -07:00
Amir Raminfar
0043ed9291 Release 4.1.1 2022-09-14 13:54:53 -07:00
Amir Raminfar
89f7d21739 Adds nowrap for date 2022-09-14 13:15:10 -07:00
Amir Raminfar
fb0b11e626 Release 4.1.0 2022-09-14 12:23:25 -07:00
Amir Raminfar
70d72060d9 Cleans up define props with typescript and cleans up css (#1879)
* Tries to clean up defineprops

* Cleans up define props for all components
2022-09-14 12:00:36 -07:00
Ángel Fernández Sánchez
cc8a7ee8e7 Update es.yml (#1877)
The variable 'logs-skipped' is added to the translation.
2022-09-14 11:56:09 -07:00
Ángel Fernández Sánchez
58197f2b23 Update pr.yml (#1878)
The variable 'logs-skipped' is added to the portuguese translation.
2022-09-14 11:55:46 -07:00
kodiakhq[bot]
d2473e0fcc Merge pull request #1876 from amir20/dependabot/docker/e2e/cypress/included-10.8.0
Bump cypress/included from 10.7.0 to 10.8.0 in /e2e
2022-09-14 09:44:08 +00:00
dependabot[bot]
3a4de053b8 Bump cypress/included from 10.7.0 to 10.8.0 in /e2e
Bumps cypress/included from 10.7.0 to 10.8.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-09-14 09:39:42 +00:00
Amir Raminfar
b97dd31c9d Does localization 2022-09-13 14:39:39 -07:00
Amir Raminfar
a632c744bc Fixes bug with search 2022-09-13 14:32:20 -07:00
Amir Raminfar
7eeb7d8600 Updates moodules 2022-09-13 14:30:22 -07:00
Amir Raminfar
8de492d16b Trims top events depending on scroll status (#1860)
* Trims top events depending on scroll status

* Cleans up models and adds better OOP around logging events

* Adds log entries being skipped component

* Fixes type errors

* Fixes tets

* Styles skipping logs
2022-09-13 14:28:53 -07:00
Amir Raminfar
2b82a0816c Removes debug from golang (#1875) 2022-09-12 09:40:55 -07:00
Amir Raminfar
25daf6b502 Uses locale date and time depending on locale (#1874)
* Uses locale date and time depending on locale

* Fixes tests
2022-09-12 09:34:24 -07:00
Ángel Fernández Sánchez
011cef8124 Spanish and Portuguese languages updates (#1872)
* Create pr.yml

* Update pr.yml

* Update es.yml

File updated with spanish translation

* Update es.yml

* Update pr.yml

Portuguese translation updated
2022-09-12 08:17:42 -07:00
kodiakhq[bot]
d06eea2c26 Merge pull request #1873 from amir20/dependabot/go_modules/github.com/docker/docker-20.10.18incompatible
Bump github.com/docker/docker from 20.10.17+incompatible to 20.10.18+incompatible
2022-09-12 09:26:56 +00:00
dependabot[bot]
cdb6738941 Bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 20.10.17+incompatible to 20.10.18+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.17...v20.10.18)

---
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-09-12 09:22:27 +00:00
Amir Raminfar
f39b6f50e3 Fixes pnpm 2022-09-11 18:48:32 -07:00
Amir Raminfar
4d03a36940 Adds layouts (#1871) 2022-09-09 14:17:58 -07:00
Amir Raminfar
60fc2ab22a Updates other modules 2022-09-09 12:40:18 -07:00
Amir Raminfar
0ad841a2d2 Updates modules for cypress 2022-09-09 12:40:01 -07:00
Amir Raminfar
58ce210924 Adds more tests 2022-09-09 12:39:41 -07:00
Amir Raminfar
0214b212ea Clean up 2022-09-08 16:16:28 -07:00
Amir Raminfar
ee37d7c30e Adds i18n support with vue-i18n (#1870)
* Adds asset changes for i18n

* Adds asset changes for i18n

* Adds vite configs

* Adds auto import and cleans up props

* Initital localzation

* Fixes dockerfile

* Fixes tests

* Updates fixutres

* Updates default lang
2022-09-08 15:40:26 -07:00
Amir Raminfar
4395bc9dc5 Cleans up main ts to modules (#1869)
* Cleans up main ts to modules

* Uses vite pages

* Updates vite-plugin-pages to prod

* Fixes title

* Fixes show

* Fixes file case

* Uses sync mode
2022-09-07 19:48:48 -07:00
kodiakhq[bot]
4ea945f0b4 Merge pull request #1868 from amir20/dependabot/docker/golang-1.19.1-alpine
Bump golang from 1.19.0-alpine to 1.19.1-alpine
2022-09-08 02:33:56 +00:00
dependabot[bot]
b221242db3 Bump golang from 1.19.0-alpine to 1.19.1-alpine
Bumps golang from 1.19.0-alpine to 1.19.1-alpine.

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-07 09:12:20 +00:00
Amir Raminfar
ad6793f614 Updates modules 2022-09-06 10:57:29 -07:00
Amir Raminfar
72f080f795 Caches builds for e2e (#1863)
* Caches builds for e2e

* Adds docker login

* Removes typo
2022-09-02 14:57:57 -07:00
Amir Raminfar
ee4210e1cc Updates modules 2022-09-02 14:45:16 -07:00
Amir Raminfar
fc68ba6391 Updates cypress 2022-08-31 10:27:58 -07:00
kodiakhq[bot]
539829d00d Merge pull request #1862 from amir20/dependabot/docker/e2e/cypress/included-10.7.0
Bump cypress/included from 10.6.0 to 10.7.0 in /e2e
2022-08-31 09:46:18 +00:00
dependabot[bot]
65c0d2a970 Bump cypress/included from 10.6.0 to 10.7.0 in /e2e
Bumps cypress/included from 10.6.0 to 10.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-08-31 09:41:35 +00:00
Amir Raminfar
696341b779 Fixes typo 2022-08-27 11:50:43 -07:00
Amir Raminfar
658efd0538 Release 4.0.2 2022-08-22 09:36:39 -07:00
Amir Raminfar
eba4cec7d6 Fixes light theme. Fixes #1855 2022-08-22 09:33:35 -07:00
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
110 changed files with 5167 additions and 3122 deletions

View File

@@ -1,2 +0,0 @@
version = 1
merge.notify_on_conflict = false

View File

@@ -1,47 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
labels:
- "dependencies"
- "automerge"
schedule:
interval: "daily"
- package-ecosystem: "docker"
directory: "/"
labels:
- "dependencies"
- "automerge"
schedule:
interval: "daily"
- package-ecosystem: gomod
directory: "/"
labels:
- "gomod"
- "dependencies"
- "automerge"
schedule:
interval: daily
- package-ecosystem: npm
directory: "/"
labels:
- "npm"
- "dependencies"
- "automerge"
schedule:
interval: daily
- package-ecosystem: npm
directory: "/e2e"
labels:
- "npm"
- "dependencies"
- "automerge"
schedule:
interval: daily
- package-ecosystem: "docker"
directory: "/e2e"
labels:
- "dependencies"
- "automerge"
schedule:
interval: "daily"

View File

@@ -13,7 +13,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v3
- name: Install pnpm
uses: pnpm/action-setup@v2.2.2
uses: pnpm/action-setup@v2.2.4
with:
version: 6.20.1
- name: Install dependencies
@@ -27,7 +27,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: 1.18.x
go-version: 1.19.x
- name: Checkout code
uses: actions/checkout@v3
- name: Run Go Tests with Coverage
@@ -52,17 +52,15 @@ jobs:
uses: docker/metadata-action@v4
with:
images: amir20/dozzle
- 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
uses: docker/setup-buildx-action@v2.2.1
- name: Login to DockerHub
uses: docker/login-action@v2.0.0
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v3.1.0
uses: docker/build-push-action@v3.2.0
with:
push: true
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
@@ -83,7 +81,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v3
- name: Install pnpm
uses: pnpm/action-setup@v2.2.2
uses: pnpm/action-setup@v2.2.4
with:
version: 6.20.1
- name: Install dependencies

View File

@@ -10,27 +10,25 @@ jobs:
buildx:
name: Push branches and PRs
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == 'amir20/dozzle' }}
if: ${{ !github.event.repository.fork && !github.event.pull_request.head.repo.fork && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == 'amir20/dozzle') }}
steps:
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: amir20/dozzle
- 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
uses: docker/setup-buildx-action@v2.2.1
- name: Login to DockerHub
uses: docker/login-action@v2.0.0
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v3.1.0
uses: docker/build-push-action@v3.2.0
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.2
uses: pnpm/action-setup@v2.2.4
with:
version: 6.20.1
- name: Install dependencies
@@ -30,7 +30,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: 1.18.x
go-version: 1.19.x
- name: Checkout code
uses: actions/checkout@v3
- name: Run Go Tests with Coverage
@@ -43,12 +43,19 @@ 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
uses: docker/setup-buildx-action@v2.2.1
- name: Login to DockerHub
if: ${{ !github.event.repository.fork && !github.event.pull_request.head.repo.fork }}
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build images
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml build
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml build --build-arg BUILDKIT_INLINE_CACHE=1
- name: Push images
if: ${{ !github.event.repository.fork && !github.event.pull_request.head.repo.fork }}
run: COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f e2e/docker-compose.yml push
- name: Set commit message for push
if: github.event_name == 'push'
run: |

1
.gitignore vendored
View File

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

View File

@@ -1,27 +1,30 @@
# Build assets
FROM --platform=$BUILDPLATFORM node:18-alpine as node
FROM --platform=$BUILDPLATFORM node:19-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
# Install dependencies from lock file
COPY pnpm-lock.yaml ./
RUN pnpm fetch --prod
RUN pnpm fetch
# Copy files
COPY package.json .* vite.config.ts index.html ./
# Copy package.json and install dependencies
COPY package.json ./
RUN pnpm install -r --offline --ignore-scripts
# Copy assets to build
# Copy assets and translations to build
COPY .* vite.config.ts index.html ./
COPY assets ./assets
COPY locales ./locales
# Install dependencies
RUN pnpm install -r --offline --prod --ignore-scripts && pnpm build
# Build assets
RUN pnpm build
FROM --platform=$BUILDPLATFORM golang:1.18.4-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.19.3-alpine AS builder
RUN apk add --no-cache git ca-certificates && mkdir /dozzle
RUN apk add --no-cache ca-certificates && mkdir /dozzle
WORKDIR /dozzle
@@ -54,8 +57,6 @@ ENV PATH /bin
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=builder /dozzle/dozzle /dozzle
HEALTHCHECK --start-period=4s --interval=2s CMD [ "/dozzle", "healthcheck" ]
EXPOSE 8080
ENTRYPOINT ["/dozzle"]

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,
@@ -74,7 +98,7 @@ this would then only allow you to view containers with a name starting with "foo
#### Authentication
Dozzle supports a very simple authentication out of the box with just username and password. You should deploy using SSL to keep the credentials safe. See configuration to use `--username` and `--password`.
Dozzle supports a very simple authentication out of the box with just username and password. You should deploy using SSL to keep the credentials safe. See configuration to use `--username` and `--password`. You can also use [docker secrets](https://docs.docker.com/engine/swarm/secrets/) `--usernamefile` and `--passwordfile`.
#### Changing base URL
@@ -105,6 +129,8 @@ Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can
| `--filter` | `DOZZLE_FILTER` | `""` |
| `--username` | `DOZZLE_USERNAME` | `""` |
| `--password` | `DOZZLE_PASSWORD` | `""` |
| `--usernamefile` | `DOZZLE_USERNAME_FILE`| `""` |
| `--passwordfile` | `DOZZLE_PASSWORD_FILE`| `""` |
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
## Troubleshooting and FAQs
@@ -157,6 +183,26 @@ Dozzle has a [special route](https://github.com/amir20/dozzle/blob/master/assets
</details>
<details>
<summary>I installed Dozzle but memory consumption doesn't show up!</summary>
*This is an issue specific to ARM devices*
Dozzle uses the Docker API to gather information about the containers' memory usage. If the memory usage is not showing up, then it is likely that the Docker API is not returning the memory usage.
You can verify this by running `docker info`, and you should see the following:
```
WARNING: No memory limit support
WARNING: No swap limit support
```
In this case, you'll need to add the following line to your `/boot/cmdline.txt` file and reboot your device.
```
cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1
```
</details>
## License
[MIT](LICENSE)

View File

@@ -1,98 +1,8 @@
<template>
<main>
<mobile-menu v-if="isMobile && !authorizationNeeded"></mobile-menu>
<splitpanes @resized="onResized($event)">
<pane min-size="10" :size="menuWidth" v-if="!authorizationNeeded && !isMobile && !collapseNav">
<side-menu @search="showFuzzySearch"></side-menu>
</pane>
<pane min-size="10">
<splitpanes>
<pane class="has-min-height router-view">
<router-view></router-view>
</pane>
<template v-if="!isMobile">
<pane v-for="other in activeContainers" :key="other.id">
<log-container
:id="other.id"
show-title
scrollable
closable
@close="containerStore.removeActiveContainer(other)"
></log-container>
</pane>
</template>
</splitpanes>
</pane>
</splitpanes>
<button
@click="collapseNav = !collapseNav"
class="button is-rounded"
:class="{ collapsed: collapseNav }"
id="hide-nav"
v-if="!isMobile && !authorizationNeeded"
>
<span class="icon ml-2" v-if="collapseNav">
<mdi-light-chevron-right />
</span>
<span class="icon" v-else>
<mdi-light-chevron-left />
</span>
</button>
</main>
<router-view></router-view>
</template>
<script lang="ts" setup>
import { Splitpanes, Pane } from "splitpanes";
import { ref, onMounted, watchEffect } from "vue";
import { storeToRefs } from "pinia";
import { useProgrammatic } from "@oruga-ui/oruga-next";
import hotkeys from "hotkeys-js";
import { setTitle } from "@/composables/title";
import { isMobile } from "@/composables/media";
import { smallerScrollbars, lightTheme, menuWidth } from "@/composables/settings";
import { useContainerStore } from "@/stores/container";
import config from "@/stores/config";
import FuzzySearchModal from "@/components/FuzzySearchModal.vue";
import LogContainer from "@/components/LogContainer.vue";
import SideMenu from "@/components/SideMenu.vue";
import MobileMenu from "@/components/MobileMenu.vue";
const collapseNav = ref(false);
const { oruga } = useProgrammatic();
const { authorizationNeeded } = config;
const containerStore = useContainerStore();
const { activeContainers, visibleContainers } = storeToRefs(containerStore);
onMounted(() => {
if (smallerScrollbars.value) {
document.documentElement.classList.add("has-custom-scrollbars");
}
switch (lightTheme.value) {
case "dark":
document.documentElement.setAttribute("data-theme", "dark");
break;
case "light":
document.documentElement.setAttribute("data-theme", "light");
break;
default:
document.documentElement.removeAttribute("data-theme");
}
hotkeys("command+k, ctrl+k", (event, handler) => {
event.preventDefault();
showFuzzySearch();
});
});
watchEffect(() => {
setTitle(`${visibleContainers.value.length} containers`);
});
watchEffect(() => {
if (smallerScrollbars.value) {
document.documentElement.classList.add("has-custom-scrollbars");
@@ -111,59 +21,6 @@ watchEffect(() => {
document.documentElement.removeAttribute("data-theme");
}
});
function showFuzzySearch() {
oruga.modal.open({
// parent: this,
component: FuzzySearchModal,
animation: "false",
width: 600,
active: true,
});
}
function onResized(e) {
if (e.length == 2) {
menuWidth.value = e[0].size;
}
}
</script>
<style scoped lang="scss">
:deep(.splitpanes--vertical > .splitpanes__splitter) {
min-width: 3px;
background: var(--border-color);
&:hover {
background: var(--border-hover-color);
}
}
@media screen and (max-width: 768px) {
.router-view {
padding-top: 75px;
}
}
.button.has-no-border {
border-color: transparent !important;
}
.has-min-height {
min-height: 100vh;
}
#hide-nav {
position: fixed;
left: 10px;
bottom: 10px;
&.collapsed {
left: -40px;
width: 60px;
padding-left: 40px;
background: rgba(0, 0, 0, 0.95);
&:hover {
left: -25px;
}
}
}
</style>
<style scoped lang="scss"></style>

628
assets/auto-imports.d.ts vendored Normal file
View File

@@ -0,0 +1,628 @@
// Generated by 'unplugin-auto-import'
export {}
declare global {
const $$: typeof import('vue/macros')['$$']
const $: typeof import('vue/macros')['$']
const $computed: typeof import('vue/macros')['$computed']
const $customRef: typeof import('vue/macros')['$customRef']
const $ref: typeof import('vue/macros')['$ref']
const $shallowRef: typeof import('vue/macros')['$shallowRef']
const $toRef: typeof import('vue/macros')['$toRef']
const DEFAULT_SETTINGS: typeof import('./composables/settings')['DEFAULT_SETTINGS']
const EffectScope: typeof import('vue')['EffectScope']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const arrayEquals: typeof import('./utils/index')['arrayEquals']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const collapseNav: typeof import('./composables/settings')['collapseNav']
const computed: typeof import('vue')['computed']
const computedAsync: typeof import('@vueuse/core')['computedAsync']
const computedEager: typeof import('@vueuse/core')['computedEager']
const computedInject: typeof import('@vueuse/core')['computedInject']
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
const config: typeof import('./stores/config')['default']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: typeof import('vue')['createApp']
const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createPinia: typeof import('pinia')['createPinia']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
const flattenJSON: typeof import('./utils/index')['flattenJSON']
const formatBytes: typeof import('./utils/index')['formatBytes']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const getDeep: typeof import('./utils/index')['getDeep']
const h: typeof import('vue')['h']
const hourStyle: typeof import('./composables/settings')['hourStyle']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const isDefined: typeof import('@vueuse/core')['isDefined']
const isMobile: typeof import('./composables/media')['isMobile']
const isObject: typeof import('./utils/index')['isObject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const lightTheme: typeof import('./composables/settings')['lightTheme']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
const mapState: typeof import('pinia')['mapState']
const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw']
const menuWidth: typeof import('./composables/settings')['menuWidth']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onLongPress: typeof import('@vueuse/core')['onLongPress']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const persistentVisibleKeys: typeof import('./utils/index')['persistentVisibleKeys']
const provide: typeof import('vue')['provide']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
const reactivePick: typeof import('@vueuse/core')['reactivePick']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
const refDebounced: typeof import('@vueuse/core')['refDebounced']
const refDefault: typeof import('@vueuse/core')['refDefault']
const refThrottled: typeof import('@vueuse/core')['refThrottled']
const refWithControl: typeof import('@vueuse/core')['refWithControl']
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveDirective: typeof import('vue')['resolveDirective']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const search: typeof import('./composables/settings')['search']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const setTitle: typeof import('./composables/title')['setTitle']
const settings: typeof import('./composables/settings')['settings']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const showAllContainers: typeof import('./composables/settings')['showAllContainers']
const showTimestamp: typeof import('./composables/settings')['showTimestamp']
const size: typeof import('./composables/settings')['size']
const smallerScrollbars: typeof import('./composables/settings')['smallerScrollbars']
const softWrap: typeof import('./composables/settings')['softWrap']
const storeToRefs: typeof import('pinia')['storeToRefs']
const stripVersion: typeof import('./utils/index')['stripVersion']
const syncRef: typeof import('@vueuse/core')['syncRef']
const syncRefs: typeof import('@vueuse/core')['syncRefs']
const templateRef: typeof import('@vueuse/core')['templateRef']
const throttledRef: typeof import('@vueuse/core')['throttledRef']
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
const toRaw: typeof import('vue')['toRaw']
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
const unref: typeof import('vue')['unref']
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
const useArraySome: typeof import('@vueuse/core')['useArraySome']
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: typeof import('@vueuse/core')['useBattery']
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: typeof import('@vueuse/core')['useCached']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useContainerStore: typeof import('./stores/container')['useContainerStore']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar']
const useCssVars: typeof import('vue')['useCssVars']
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
const useCycleList: typeof import('@vueuse/core')['useCycleList']
const useDark: typeof import('@vueuse/core')['useDark']
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
const useDebounce: typeof import('@vueuse/core')['useDebounce']
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
const useDraggable: typeof import('@vueuse/core')['useDraggable']
const useDropZone: typeof import('@vueuse/core')['useDropZone']
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
const useElementHover: typeof import('@vueuse/core')['useElementHover']
const useElementSize: typeof import('@vueuse/core')['useElementSize']
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
const useEventBus: typeof import('@vueuse/core')['useEventBus']
const useEventListener: typeof import('@vueuse/core')['useEventListener']
const useEventSource: typeof import('@vueuse/core')['useEventSource']
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
const useFavicon: typeof import('@vueuse/core')['useFavicon']
const useFetch: typeof import('@vueuse/core')['useFetch']
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
const useFocus: typeof import('@vueuse/core')['useFocus']
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
const useFps: typeof import('@vueuse/core')['useFps']
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useHead: typeof import('@vueuse/head')['useHead']
const useI18n: typeof import('vue-i18n')['useI18n']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
const useInterval: typeof import('@vueuse/core')['useInterval']
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLink: typeof import('vue-router')['useLink']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useLogStream: typeof import('./composables/eventsource')['useLogStream']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
const useNetwork: typeof import('@vueuse/core')['useNetwork']
const useNow: typeof import('@vueuse/core')['useNow']
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useParallax: typeof import('@vueuse/core')['useParallax']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSearchFilter: typeof import('./composables/search')['useSearchFilter']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useSlots: typeof import('vue')['useSlots']
const useSorted: typeof import('@vueuse/core')['useSorted']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useStepper: typeof import('@vueuse/core')['useStepper']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
const useThrottle: typeof import('@vueuse/core')['useThrottle']
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToNumber: typeof import('@vueuse/core')['useToNumber']
const useToString: typeof import('@vueuse/core')['useToString']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
const useVModel: typeof import('@vueuse/core')['useVModel']
const useVModels: typeof import('@vueuse/core')['useVModels']
const useVibrate: typeof import('@vueuse/core')['useVibrate']
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
const useVisibleFilter: typeof import('./composables/visible')['useVisibleFilter']
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
const watch: typeof import('vue')['watch']
const watchArray: typeof import('@vueuse/core')['watchArray']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
const watchEffect: typeof import('vue')['watchEffect']
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchPausable: typeof import('@vueuse/core')['watchPausable']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: typeof import('@vueuse/core')['whenever']
}
// for vue template auto import
import { UnwrapRef } from 'vue'
declare module 'vue' {
interface ComponentCustomProperties {
readonly $$: UnwrapRef<typeof import('vue/macros')['$$']>
readonly $: UnwrapRef<typeof import('vue/macros')['$']>
readonly $computed: UnwrapRef<typeof import('vue/macros')['$computed']>
readonly $customRef: UnwrapRef<typeof import('vue/macros')['$customRef']>
readonly $ref: UnwrapRef<typeof import('vue/macros')['$ref']>
readonly $shallowRef: UnwrapRef<typeof import('vue/macros')['$shallowRef']>
readonly $toRef: UnwrapRef<typeof import('vue/macros')['$toRef']>
readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./composables/settings')['DEFAULT_SETTINGS']>
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly collapseNav: UnwrapRef<typeof import('./composables/settings')['collapseNav']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
readonly config: UnwrapRef<typeof import('./stores/config')['default']>
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly flattenJSON: UnwrapRef<typeof import('./utils/index')['flattenJSON']>
readonly formatBytes: UnwrapRef<typeof import('./utils/index')['formatBytes']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getDeep: UnwrapRef<typeof import('./utils/index')['getDeep']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly hourStyle: UnwrapRef<typeof import('./composables/settings')['hourStyle']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isMobile: UnwrapRef<typeof import('./composables/media')['isMobile']>
readonly isObject: UnwrapRef<typeof import('./utils/index')['isObject']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly lightTheme: UnwrapRef<typeof import('./composables/settings')['lightTheme']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly menuWidth: UnwrapRef<typeof import('./composables/settings')['menuWidth']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly persistentVisibleKeys: UnwrapRef<typeof import('./utils/index')['persistentVisibleKeys']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']>
readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
readonly ref: UnwrapRef<typeof import('vue')['ref']>
readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveDirective: UnwrapRef<typeof import('vue')['resolveDirective']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly search: UnwrapRef<typeof import('./composables/settings')['search']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly setTitle: UnwrapRef<typeof import('./composables/title')['setTitle']>
readonly settings: UnwrapRef<typeof import('./composables/settings')['settings']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly showAllContainers: UnwrapRef<typeof import('./composables/settings')['showAllContainers']>
readonly showTimestamp: UnwrapRef<typeof import('./composables/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./composables/settings')['size']>
readonly smallerScrollbars: UnwrapRef<typeof import('./composables/settings')['smallerScrollbars']>
readonly softWrap: UnwrapRef<typeof import('./composables/settings')['softWrap']>
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
readonly stripVersion: UnwrapRef<typeof import('./utils/index')['stripVersion']>
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']>
readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']>
readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']>
readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']>
readonly unref: UnwrapRef<typeof import('vue')['unref']>
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useContainerStore: UnwrapRef<typeof import('./stores/container')['useContainerStore']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']>
readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']>
readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']>
readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']>
readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']>
readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']>
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']>
readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']>
readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useHead: UnwrapRef<typeof import('@vueuse/head')['useHead']>
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useLogStream: UnwrapRef<typeof import('./composables/eventsource')['useLogStream']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSearchFilter: UnwrapRef<typeof import('./composables/search')['useSearchFilter']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
readonly useVisibleFilter: UnwrapRef<typeof import('./composables/visible')['useVisibleFilter']>
readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>
readonly watch: UnwrapRef<typeof import('vue')['watch']>
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']>
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
}
}

View File

@@ -10,21 +10,26 @@ declare module '@vue/runtime-core' {
CarbonCaretDown: typeof import('~icons/carbon/caret-down')['default']
CilColumns: typeof import('~icons/cil/columns')['default']
CilFindInPage: typeof import('~icons/cil/find-in-page')['default']
ContainerStat: typeof import('./components/ContainerStat.vue')['default']
ContainerTitle: typeof import('./components/ContainerTitle.vue')['default']
ComplexLogItem: typeof import('./components/LogViewer/ComplexLogItem.vue')['default']
ContainerStat: typeof import('./components/LogViewer/ContainerStat.vue')['default']
ContainerTitle: typeof import('./components/LogViewer/ContainerTitle.vue')['default']
DockerEventLogItem: typeof import('./components/LogViewer/DockerEventLogItem.vue')['default']
DropdownMenu: typeof import('./components/DropdownMenu.vue')['default']
FieldList: typeof import('./components/LogViewer/FieldList.vue')['default']
FuzzySearchModal: typeof import('./components/FuzzySearchModal.vue')['default']
InfiniteLoader: typeof import('./components/InfiniteLoader.vue')['default']
LogActionsToolbar: typeof import('./components/LogActionsToolbar.vue')['default']
LogContainer: typeof import('./components/LogContainer.vue')['default']
LogEventSource: typeof import('./components/LogEventSource.vue')['default']
LogViewer: typeof import('./components/LogViewer.vue')['default']
LogViewerWithSource: typeof import('./components/LogViewerWithSource.vue')['default']
LogActionsToolbar: typeof import('./components/LogViewer/LogActionsToolbar.vue')['default']
LogContainer: typeof import('./components/LogViewer/LogContainer.vue')['default']
LogDate: typeof import('./components/LogViewer/LogDate.vue')['default']
LogEventSource: typeof import('./components/LogViewer/LogEventSource.vue')['default']
LogViewer: typeof import('./components/LogViewer/LogViewer.vue')['default']
LogViewerWithSource: typeof import('./components/LogViewer/LogViewerWithSource.vue')['default']
MdiDotsVertical: typeof import('~icons/mdi/dots-vertical')['default']
MdiLightChevronDoubleDown: typeof import('~icons/mdi-light/chevron-double-down')['default']
MdiLightChevronLeft: typeof import('~icons/mdi-light/chevron-left')['default']
MdiLightChevronRight: typeof import('~icons/mdi-light/chevron-right')['default']
MdiLightCog: typeof import('~icons/mdi-light/cog')['default']
MdiLightLogout: typeof import('~icons/mdi-light/logout')['default']
MdiLightMagnify: typeof import('~icons/mdi-light/magnify')['default']
MobileMenu: typeof import('./components/MobileMenu.vue')['default']
OcticonContainer24: typeof import('~icons/octicon/container24')['default']
@@ -38,5 +43,10 @@ declare module '@vue/runtime-core' {
ScrollProgress: typeof import('./components/ScrollProgress.vue')['default']
Search: typeof import('./components/Search.vue')['default']
SideMenu: typeof import('./components/SideMenu.vue')['default']
SimpleLogItem: typeof import('./components/LogViewer/SimpleLogItem.vue')['default']
SkippedEntriesLogItem: typeof import('./components/LogViewer/SkippedEntriesLogItem.vue')['default']
StatMonitor: typeof import('./components/LogViewer/StatMonitor.vue')['default']
StatSparkline: typeof import('./components/LogViewer/StatSparkline.vue')['default']
ZigZag: typeof import('./components/LogViewer/ZigZag.vue')['default']
}
}

View File

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

View File

@@ -3,26 +3,29 @@
<o-autocomplete
ref="autocomplete"
v-model="query"
placeholder="Search containers using ⌘ + k or ctrl + k"
field="name"
:placeholder="$t('placeholder.search-containers')"
open-on-focus
keep-first
expanded
:data="results"
:data="data"
@select="selected"
>
<template #default="props">
<template #default="{ option: item }">
<div class="media">
<div class="media-left">
<span class="icon is-small" :class="props.option.state">
<span class="icon is-small" :class="item.state">
<octicon-container-24 />
</span>
</div>
<div class="media-content">
{{ props.option.name }}
{{ item.name }}
</div>
<div class="media-right">
<span class="icon is-small column-icon" @click.stop.prevent="addColumn(props.option)" title="Pin as column">
<span
class="icon is-small column-icon"
@click.stop.prevent="addColumn(item)"
:title="$t('tooltip.pin-column')"
>
<cil-columns />
</span>
</div>
@@ -33,62 +36,61 @@
</template>
<script lang="ts" setup>
import fuzzysort from "fuzzysort";
import { computed, nextTick, onMounted, ref, reactive } from "vue";
import { useRouter } from "vue-router";
import { useContainerStore } from "@/stores/container";
import { storeToRefs } from "pinia";
import { Container } from "@/types/Container";
import { Container } from "@/models/Container";
import { useFuse } from "@vueuse/integrations/useFuse";
const props = defineProps({
maxResults: {
default: 20,
type: Number,
},
});
const { maxResults: resultLimit = 20 } = defineProps<{
maxResults?: number;
}>();
const emit = defineEmits(["close"]);
const emit = defineEmits<{
(e: "close"): void;
}>();
const query = ref("");
const autocomplete = ref<HTMLElement>();
const router = useRouter();
const store = useContainerStore();
const { containers } = storeToRefs(store);
const preparedContainers = computed(() =>
containers.value.map(({ name, id, created, state }) =>
reactive({
name,
const list = computed(() => {
return containers.value.map(({ id, created, name, state }) => {
return {
id,
created,
name,
state,
preparedName: fuzzysort.prepare(name),
})
)
);
const results = computed(() => {
const options = {
limit: props.maxResults,
key: "preparedName",
};
if (query.value) {
const results = fuzzysort.go(query.value, preparedContainers.value, options);
results.forEach((result) => {
if (result.obj.state === "running") {
// @ts-ignore
result.score += 1;
}
});
return [...results].sort((a, b) => b.score - a.score).map((i) => i.obj);
} else {
return [...preparedContainers.value].sort((a, b) => b.created - a.created);
}
};
});
});
onMounted(() => nextTick(() => autocomplete.value?.focus()));
const { results } = useFuse(query, list, {
fuseOptions: { keys: ["name"], includeScore: true },
resultLimit,
matchAllWhenSearchEmpty: true,
});
function selected(item: { id: string; name: string }) {
router.push({ name: "container", params: { id: item.id } });
const data = computed(() => {
return results.value
.sort((a, b) => {
if (a.score === b.score) {
if (a.item.state === "running" && b.item.state !== "running") {
return -1;
} else {
return 1;
}
} else if (a.score && b.score) {
return a.score - b.score;
} else {
return 0;
}
})
.map(({ item }) => item);
});
watchOnce(autocomplete, () => autocomplete.value?.focus());
function selected({ id }: { id: string }) {
router.push({ name: "container-id", params: { id } });
emit("close");
}
function addColumn(container: Container) {
@@ -103,6 +105,15 @@ function addColumn(container: Container) {
width: 580px;
}
@media screen and (max-width: 768px) {
.panel {
min-height: 200px;
width: auto;
margin-left: 0.25rem!important;
margin-right: 0.25rem!important;
}
}
.running {
color: var(--primary-color);
}

View File

@@ -9,30 +9,28 @@
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, nextTick } from "vue";
const props = defineProps({
onLoadMore: Function,
enabled: Boolean,
});
const { onLoadMore = () => {}, enabled } = defineProps<{
onLoadMore: () => void;
enabled: boolean;
}>();
const isLoading = ref(false);
const root = ref<HTMLElement>();
const observer = new IntersectionObserver(async (entries) => {
if (entries[0].intersectionRatio <= 0) return;
if (props.onLoadMore && props.enabled) {
const scrollingParent = root.value.closest("[data-scrolling]") || document.documentElement;
if (onLoadMore && enabled) {
const scrollingParent = root.value?.closest("[data-scrolling]") || document.documentElement;
const previousHeight = scrollingParent.scrollHeight;
isLoading.value = true;
await props.onLoadMore();
await onLoadMore();
isLoading.value = false;
await nextTick();
scrollingParent.scrollTop += scrollingParent.scrollHeight - previousHeight;
}
});
onMounted(() => observer.observe(root.value));
onMounted(() => observer.observe(root.value!));
onUnmounted(() => observer.disconnect());
</script>

View File

@@ -1,133 +0,0 @@
<template>
<infinite-loader :onLoadMore="loadOlderLogs" :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 { 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 messages = ref<LogEntry[]>([]);
const buffer = ref<LogEntry[]>([]);
function flushNow() {
messages.value.push(...buffer.value);
buffer.value = [];
}
const flushBuffer = debounce(flushNow, 250, { maxWait: 1000 });
let es: EventSource | null = null;
let lastEventId = "";
function connect({ clear } = { clear: true }) {
es?.close();
if (clear) {
flushBuffer.cancel();
messages.value = [];
buffer.value = [];
lastEventId = "";
}
es = new EventSource(`${config.base}/api/logs/stream?id=${props.id}&lastEventId=${lastEventId}`);
es.addEventListener("container-stopped", () => {
es?.close();
es = null;
buffer.value.push({
event: "container-stopped",
message: "Container stopped",
date: new Date(),
key: new Date().toString(),
});
flushBuffer();
flushBuffer.flush();
});
es.addEventListener("error", (e) => console.error("EventSource failed: " + JSON.stringify(e)));
es.onmessage = (e) => {
lastEventId = e.lastEventId;
if (e.data) {
buffer.value.push(parseMessage(e.data));
flushBuffer();
}
};
}
async function loadOlderLogs() {
if (messages.value.length < 300) return;
emit("loading-more", true);
const to = messages.value[0].date;
const last = messages.value[299].date;
const delta = to.getTime() - last.getTime();
const from = new Date(to.getTime() + delta);
const logs = await (
await fetch(`${config.base}/api/logs?id=${props.id}&from=${from.toISOString()}&to=${to.toISOString()}`)
).text();
if (logs) {
const newMessages = logs
.trim()
.split("\n")
.map((line) => parseMessage(line));
messages.value.unshift(...newMessages);
}
emit("loading-more", false);
}
function parseMessage(data: String): LogEntry {
let i = data.indexOf(" ");
if (i == -1) {
i = data.length;
}
const key = data.substring(0, i);
const date = new Date(key);
const message = data.substring(i + 1);
return { key, date, message };
}
watch(
() => container.value.state,
(newValue, oldValue) => {
console.log("LogEventSource: container changed", newValue, oldValue);
if (newValue == "running" && newValue != oldValue) {
buffer.value.push({
event: "container-started",
message: "Container started",
date: new Date(),
key: new Date().toString(),
});
connect({ clear: false });
}
}
);
onUnmounted(() => {
if (es) {
es.close();
}
});
connect();
watch(id, () => connect());
defineExpose({
clear: () => (messages.value = []),
});
</script>

View File

@@ -1,192 +0,0 @@
<template>
<ul class="events" ref="events" :class="{ 'disable-wrap': !softWrap, [size]: true }">
<li
v-for="(item, index) in filtered"
:key="item.key"
:data-key="item.key"
:data-event="item.event"
:class="{ selected: item.selected }"
>
<div class="line-options" v-show="isSearching()">
<dropdown-menu :class="{ 'is-last': index === filtered.length - 1 }" class="is-top minimal">
<a class="dropdown-item" @click="handleJumpLineSelected($event, item)" :href="`#${item.key}`">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<cil-find-in-page class="mr-4" />
</div>
</div>
<div class="level-right">
<div class="level-item">Jump to Context</div>
</div>
</div>
</a>
</dropdown-menu>
</div>
<div class="line">
<span class="date" v-if="showTimestamp"> <relative-time :date="item.date"></relative-time></span>
<span class="text" v-html="colorize(item.message)"></span>
</div>
</li>
</ul>
</template>
<script lang="ts" setup>
import { PropType, ref, toRefs, watch } from "vue";
import { useRouteHash } from "@vueuse/router";
import { size, showTimestamp, softWrap } from "@/composables/settings";
import RelativeTime from "./RelativeTime.vue";
import AnsiConvertor from "ansi-to-html";
import { LogEntry } from "@/types/LogEntry";
import { useSearchFilter } from "@/composables/search";
const props = defineProps({
messages: {
type: Array as PropType<LogEntry[]>,
required: true,
},
});
const ansiConvertor = new AnsiConvertor({ escapeXML: true });
const { filteredMessages, resetSearch, markSearch, isSearching } = useSearchFilter();
const colorize = (value: string) => markSearch(ansiConvertor.toHtml(value));
const { messages } = toRefs(props);
const filtered = filteredMessages(messages);
const events = ref<HTMLElement>();
let lastSelectedItem: LogEntry | undefined = undefined;
function handleJumpLineSelected(e: Event, item: LogEntry) {
if (lastSelectedItem) {
lastSelectedItem.selected = false;
}
lastSelectedItem = item;
item.selected = true;
resetSearch();
}
const routeHash = useRouteHash();
watch(
routeHash,
(hash) => {
document.querySelector(`[data-key="${hash.substring(1)}"]`)?.scrollIntoView({ block: "center" });
},
{ immediate: true, flush: "post" }
);
</script>
<style scoped lang="scss">
.events {
padding: 1em 0;
font-family: SFMono-Regular, Consolas, Liberation Mono, monaco, Menlo, monospace;
&.disable-wrap {
.line,
.text {
white-space: nowrap;
}
}
& > li {
display: flex;
word-wrap: break-word;
padding: 0.2em 1em;
&:last-child {
scroll-snap-align: end;
scroll-margin-block-end: 5rem;
}
&:nth-child(odd) {
background-color: rgba(125, 125, 125, 0.08);
}
&[data-event="container-stopped"] {
color: #f14668;
}
&[data-event="container-started"] {
color: hsl(141, 53%, 53%);
}
&.selected .date {
background-color: var(--menu-item-active-background-color);
color: var(--text-color);
}
&.selected > .date {
background-color: white;
}
& > .line {
margin: auto 0;
width: 100%;
}
& > .line-options {
display: flex;
flex-direction: row-reverse;
margin-right: 1em;
}
}
&.small {
font-size: 60%;
}
&.medium {
font-size: 80%;
}
&.large {
font-size: 120%;
}
}
@media (prefers-color-scheme: dark) {
.date {
background-color: #262626;
color: #258ccd;
}
}
[data-theme="dark"] {
.date {
background-color: #262626;
color: #258ccd;
}
}
@media (prefers-color-scheme: light) {
.date {
background-color: #f0f0f0;
color: #009900;
}
}
[data-theme="light"] {
.date {
background-color: #f0f0f0;
color: #009900;
}
}
.date {
padding-left: 5px;
padding-right: 5px;
border-radius: 3px;
}
.text {
white-space: pre-wrap;
&::before {
content: " ";
}
}
:deep(mark) {
border-radius: 2px;
background-color: var(--secondary-color);
animation: pops 200ms ease-out;
display: inline-block;
}
@keyframes pops {
0% {
transform: scale(1.5);
}
100% {
transform: scale(1.05);
}
}
</style>

View File

@@ -0,0 +1,63 @@
<template>
<div class="columns is-1 is-variable">
<div class="column is-narrow" v-if="showTimestamp">
<log-date :date="logEntry.date"></log-date>
</div>
<div class="column">
<ul class="fields" :class="{ expanded }" @click="expanded = !expanded">
<li v-for="(value, name) in validValues(logEntry.message)">
<span class="has-text-grey">{{ name }}=</span>
<span class="has-text-weight-bold" v-html="markSearch(value)"></span>
</li>
</ul>
<field-list :fields="logEntry.unfilteredMessage" :expanded="expanded" :visible-keys="visibleKeys"></field-list>
</div>
</div>
</template>
<script lang="ts" setup>
import { type ComplexLogEntry } from "@/models/LogEntry";
const { markSearch } = useSearchFilter();
const { logEntry } = defineProps<{
logEntry: ComplexLogEntry;
visibleKeys: string[][];
}>();
let expanded = $ref(false);
function validValues(obj: Record<string, any>) {
return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== undefined));
}
</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;
}
}
&.expanded:hover {
&::after {
content: "collapse json";
}
}
li {
display: inline-block;
& + li {
margin-left: 1em;
}
}
}
</style>

View File

@@ -0,0 +1,76 @@
<template>
<div class="is-size-7 is-uppercase columns is-marginless is-mobile is-vcentered" v-if="container.stat">
<stat-monitor
class="column is-narrow"
:data="memoryData"
label="mem"
:stat-value="formatBytes(container.stat.memoryUsage)"
></stat-monitor>
<stat-monitor
class="column is-narrow"
:data="cpuData"
label="load"
:stat-value="container.stat.cpu + '%'"
></stat-monitor>
</div>
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
import { type ComputedRef } from "vue";
const container = inject("container") as ComputedRef<Container>;
const cpuData = computedWithControl(
() => container.value.getLastStat(),
() => {
const history = container.value.getStatHistory();
const points: Point<unknown>[] = history.map((stat, i) => ({
x: i,
y: stat.snapshot.cpu,
value: stat.snapshot.cpu + "%",
}));
return points;
}
);
const memoryData = computedWithControl(
() => container.value.getLastStat(),
() => {
const history = container.value.getStatHistory();
const points: Point<string>[] = history.map((stat, i) => ({
x: i,
y: stat.snapshot.memory,
value: formatBytes(stat.snapshot.memoryUsage),
}));
return points;
}
);
</script>
<style lang="scss" scoped>
.has-spacer {
&::after {
content: " ";
}
}
.has-border {
border: 1px solid var(--primary-color);
border-radius: 3px;
padding: 1px 1px 0 1px;
display: flex;
overflow: hidden;
padding-top: 0.25em;
}
.has-background-body-color {
background-color: var(--body-background-color);
}
.is-top-left {
position: absolute;
top: 0;
left: 0.75em;
}
</style>

View File

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

View File

@@ -0,0 +1,24 @@
<template>
<span class="text" :data-event="logEntry.event" v-html="logEntry.message"></span>
</template>
<script lang="ts" setup>
import { DockerEventLogEntry } from "@/models/LogEntry";
defineProps<{
logEntry: DockerEventLogEntry;
}>();
</script>
<style lang="scss" scoped>
span {
&[data-event="container-stopped"] {
color: #f14668;
}
&[data-event="container-started"] {
color: hsl(141, 53%, 53%);
}
&.text {
white-space: pre-wrap;
}
}
</style>

View File

@@ -0,0 +1,76 @@
<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";
const {
fields,
expanded = false,
parentKey = [],
visibleKeys = [],
} = defineProps<{
fields: Record<string, any>;
expanded?: boolean;
parentKey?: string[];
visibleKeys?: string[][];
}>();
const root = ref<HTMLElement>();
async function toggleField(field: string) {
const index = fieldIndex(field);
if (index > -1) {
visibleKeys.splice(index, 1);
} else {
visibleKeys.push(parentKey.concat(field));
}
await nextTick();
root.value?.scrollIntoView({
block: "center",
});
}
function hasField(field: string) {
return fieldIndex(field) > -1;
}
function fieldIndex(field: string) {
const path = parentKey.concat(field);
return visibleKeys.findIndex((keys) => arrayEquals(toRaw(keys), toRaw(path)));
}
</script>
<style lang="scss" scoped>
ul {
margin-left: 2em;
}
</style>

View File

@@ -8,7 +8,7 @@
</div>
</div>
<div class="level-right">
<div class="level-item">Clear</div>
<div class="level-item">{{ $t("toolbar.clear") }}</div>
</div>
</div>
</a>
@@ -20,7 +20,7 @@
</div>
</div>
<div class="level-right">
<div class="level-item">Download</div>
<div class="level-item">{{ $t("toolbar.download") }}</div>
</div>
</div>
</a>
@@ -33,7 +33,7 @@
</div>
</div>
<div class="level-right">
<div class="level-item">Search</div>
<div class="level-item">{{ $t("toolbar.search") }}</div>
</div>
</div>
</a>
@@ -41,34 +41,17 @@
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, PropType } from "vue";
import hotkeys from "hotkeys-js";
import config from "@/stores/config";
import { Container } from "@/types/Container";
import { useSearchFilter } from "@/composables/search";
import { type ComputedRef } from "vue";
import { Container } from "@/models/Container";
const { showSearch } = useSearchFilter();
const { base } = config;
const props = defineProps({
onClearClicked: {
type: Function as PropType<(e: Event) => void>,
default: (e: Event) => {},
},
container: {
type: Object as () => Container,
required: true,
},
});
const { onClearClicked = (e: Event) => {} } = defineProps<{
onClearClicked: (e: Event) => void;
}>();
const onHotkey = (event: Event) => {
props.onClearClicked(event);
event.preventDefault();
};
onMounted(() => hotkeys("shift+command+l, shift+ctrl+l", onHotkey));
onUnmounted(() => hotkeys.unbind("shift+command+l, shift+ctrl+l", onHotkey));
const container = inject("container") as ComputedRef<Container>;
</script>
<style lang="scss" scoped>

View File

@@ -1,16 +1,16 @@
<template>
<scrollable-view :scrollable="scrollable" v-if="container">
<template #header v-if="showTitle">
<div class="mr-0 columns is-vcentered is-marginless is-hidden-mobile">
<div class="mr-0 columns is-vcentered is-marginless is-hidden-mobile has-boxshadow">
<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 />
</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,41 +18,35 @@
</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 LogViewerWithSource from "./LogViewerWithSource.vue";
import { useContainerStore } from "@/stores/container";
const props = defineProps({
id: {
type: String,
required: true,
},
showTitle: {
type: Boolean,
default: false,
},
scrollable: {
type: Boolean,
default: false,
},
closable: {
type: Boolean,
default: false,
},
});
const {
id,
showTitle = false,
scrollable = false,
closable = false,
} = defineProps<{
id: string;
showTitle?: boolean;
scrollable?: boolean;
closable?: boolean;
}>();
const emit = defineEmits(["close"]);
const emit = defineEmits<{
(event: "close"): void;
}>();
const { id } = toRefs(props);
const store = useContainerStore();
const container = store.currentContainer(id);
const container = store.currentContainer($$(id));
provide("container", container);
const viewer = ref<InstanceType<typeof LogViewerWithSource>>();

View File

@@ -0,0 +1,19 @@
<template>
<relative-time :date="date" class="date"></relative-time>
</template>
<script lang="ts" setup>
defineProps<{
date: Date;
}>();
</script>
<style lang="scss" scoped>
.date {
padding-left: 5px;
padding-right: 5px;
border-radius: 3px;
white-space: nowrap;
background-color: var(--scheme-main-ter);
color: #258ccd;
}
</style>

View File

@@ -4,31 +4,12 @@ import { createTestingPinia } from "@pinia/testing";
import EventSource, { sources } from "eventsourcemock";
import LogEventSource from "./LogEventSource.vue";
import LogViewer from "./LogViewer.vue";
import { settings } from "../composables/settings";
import { 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, 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: "" },
@@ -42,11 +23,19 @@ describe("<LogEventSource />", () => {
beforeEach(() => {
global.EventSource = EventSource;
// @ts-ignore
window.scrollTo = vi.fn();
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
disconnect: vi.fn(),
}));
vi.useFakeTimers();
vi.setSystemTime(1560336942459);
});
afterEach(() => {
vi.restoreAllMocks();
vi.useRealTimers();
});
function createLogEventSource(
@@ -59,6 +48,9 @@ describe("<LogEventSource />", () => {
) {
settings.value.hourStyle = hourStyle;
search.searchFilter.value = searchFilter;
if(searchFilter){
search.showSearch.value = true;
}
const router = createRouter({
history: createWebHistory("/"),
@@ -78,13 +70,17 @@ describe("<LogEventSource />", () => {
components: {
LogViewer,
},
provide: {
container: computed(() => ({ id: "abc", image: "test:v123" })),
scrollingPaused: computed(() => false),
},
},
slots: {
default: `
<template #scoped="params"><log-viewer :messages="params.messages"></log-viewer></template>
`,
},
props: { id: "abc" },
props: {},
});
}
@@ -111,68 +107,28 @@ 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}`,
});
vi.runAllTimers();
await nextTick();
// @ts-ignore
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` });
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 +136,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 +149,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 +162,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 +175,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 +188,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

@@ -0,0 +1,25 @@
<template>
<infinite-loader :onLoadMore="fetchMore" :enabled="messages.length > 100"></infinite-loader>
<slot :messages="messages"></slot>
</template>
<script lang="ts" setup>
import { Container } from "@/models/Container";
import { type ComputedRef } from "vue";
const emit = defineEmits<{
(e: "loading-more", value: boolean): void;
}>();
const container = inject("container") as ComputedRef<Container>;
const { messages, loadOlderLogs } = useLogStream(container);
const beforeLoading = () => emit("loading-more", true);
const afterLoading = () => emit("loading-more", false);
defineExpose({
clear: () => (messages.value = []),
});
const fetchMore = () => loadOlderLogs({ beforeLoading, afterLoading });
</script>

View File

@@ -0,0 +1,106 @@
<template>
<ul class="events" ref="events" :class="{ 'disable-wrap': !softWrap, [size]: true }">
<li
v-for="(item, index) in filtered"
:key="item.id"
:data-key="item.id"
: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.id}`">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<cil-find-in-page class="mr-4" />
</div>
</div>
<div class="level-right">
<div class="level-item">Jump to Context</div>
</div>
</div>
</a>
</dropdown-menu>
</div>
<component :is="item.getComponent()" :log-entry="item" :visible-keys="visibleKeys.value"></component>
</li>
</ul>
</template>
<script lang="ts" setup>
import { type ComputedRef, toRaw } from "vue";
import { useRouteHash } from "@vueuse/router";
import { Container } from "@/models/Container";
import { type JSONObject, LogEntry } from "@/models/LogEntry";
const props = defineProps<{
messages: LogEntry<string | JSONObject>[];
}>();
let visibleKeys = persistentVisibleKeys(inject("container") as ComputedRef<Container>);
const { filteredPayload } = useVisibleFilter(visibleKeys);
const { filteredMessages, resetSearch, isSearching } = useSearchFilter();
const { messages } = toRefs(props);
const visible = filteredPayload(messages);
const filtered = filteredMessages(visible);
const events = ref<HTMLElement>();
let lastSelectedItem: LogEntry<string | JSONObject> | undefined = $ref(undefined);
function handleJumpLineSelected(e: Event, item: LogEntry<string | JSONObject>) {
lastSelectedItem = item;
resetSearch();
}
const routeHash = useRouteHash();
watch(
routeHash,
(hash) => {
document.querySelector(`[data-key="${hash.substring(1)}"]`)?.scrollIntoView({ block: "center" });
},
{ immediate: true, flush: "post" }
);
</script>
<style scoped lang="scss">
.events {
padding: 1em 0;
font-family: SFMono-Regular, Consolas, Liberation Mono, monaco, Menlo, monospace;
& > li {
display: flex;
word-wrap: break-word;
padding: 0.2em 1em;
&:last-child {
scroll-snap-align: end;
scroll-margin-block-end: 5rem;
}
&:nth-child(odd) {
background-color: rgba(125, 125, 125, 0.08);
}
&.selected {
border: 1px var(--secondary-color) solid;
}
& > .line-options {
display: flex;
flex-direction: row-reverse;
margin-right: 1em;
}
}
&.small {
font-size: 60%;
}
&.medium {
font-size: 80%;
}
&.large {
font-size: 120%;
}
}
</style>

View File

@@ -0,0 +1,21 @@
<template>
<log-event-source ref="source" #default="{ messages }" @loading-more="emit('loading-more', $event)">
<log-viewer :messages="messages"></log-viewer>
</log-event-source>
</template>
<script lang="ts" setup>
import LogEventSource from "./LogEventSource.vue";
const emit = defineEmits<{
(e: "loading-more", value: boolean): void;
}>();
const source = $ref<InstanceType<typeof LogEventSource>>();
function clear() {
source?.clear();
}
defineExpose({
clear,
});
</script>

View File

@@ -0,0 +1,32 @@
<template>
<div class="columns is-1 is-variable">
<div class="column is-narrow" v-if="showTimestamp">
<log-date :date="logEntry.date"></log-date>
</div>
<div class="text column" v-html="colorize(logEntry.message)"></div>
</div>
</template>
<script lang="ts" setup>
import { SimpleLogEntry } from "@/models/LogEntry";
import AnsiConvertor from "ansi-to-html";
const ansiConvertor = new AnsiConvertor({ escapeXML: true });
defineProps<{
logEntry: SimpleLogEntry;
}>();
const { markSearch } = useSearchFilter();
const colorize = (value: string) => markSearch(ansiConvertor.toHtml(value));
</script>
<style lang="scss" scoped>
.disable-wrap {
.text {
white-space: nowrap;
}
}
.text {
white-space: pre-wrap;
}
</style>

View File

@@ -0,0 +1,23 @@
<template>
<div class="is-flex-grow-1 has-text-centered my-4">
<div class="is-relative">
<zig-zag class="is-overlay mt-2"></zig-zag>
<span class="text is-relative py-2 px-4">{{ $t("error.logs-skipped", { total: logEntry.totalSkipped }) }}</span>
</div>
</div>
</template>
<script lang="ts" setup>
import { SkippedLogsEntry } from "@/models/LogEntry";
defineProps<{
logEntry: SkippedLogsEntry;
}>();
</script>
<style lang="scss" scoped>
.text {
font-weight: bold;
white-space: pre-wrap;
background-color: var(--body-background-color);
}
</style>

View File

@@ -0,0 +1,56 @@
<template>
<div class="has-text-centered is-relative host" @mouseenter="mouseOver = true" @mouseleave="mouseOver = false">
<div class="has-border has-boxshadow">
<stat-sparkline :data="data" @selected-point="onSelectedPoint"></stat-sparkline>
</div>
<div class="has-background-body-color is-top-left">
<span class="has-text-weight-light has-spacer">{{ label }}</span>
<span class="has-text-weight-bold">
{{ mouseOver ? selectedPoint?.value ?? selectedPoint?.y ?? statValue : statValue }}
</span>
</div>
</div>
</template>
<script lang="ts" setup>
const { data, label, statValue } = defineProps<{ data: Point<unknown>[]; label: string; statValue: string | number }>();
let selectedPoint: Point<unknown> = $ref();
function onSelectedPoint(point: Point<unknown>) {
selectedPoint = point;
}
let mouseOver = $ref(false);
</script>
<style lang="scss" scoped>
.has-spacer {
&::after {
content: " ";
}
}
.has-border {
border: 1px solid var(--primary-color);
border-radius: 3px;
padding: 1px 1px 0 1px;
display: flex;
overflow: hidden;
padding-top: 0.25em;
}
.has-background-body-color {
background-color: var(--body-background-color);
}
.host:hover span {
color: var(--secondary-color);
}
.is-top-left {
position: absolute;
top: 0;
left: 0.75em;
}
</style>

View File

@@ -0,0 +1,62 @@
<template>
<svg :width="width" :height="height" @mousemove="onMove">
<path :d="path" class="area" />
<line :x1="lineX" y1="0" :x2="lineX" :y2="height" class="line" />
</svg>
</template>
<script lang="ts" setup>
import { extent } from "d3-array";
import { scaleLinear } from "d3-scale";
import { area, curveStep } from "d3-shape";
const d3 = { extent, scaleLinear, area, curveStep };
const { data, width = 150, height = 30 } = defineProps<{ data: Point<unknown>[]; width?: number; height?: number }>();
const x = d3.scaleLinear().range([width, 0]);
const y = d3.scaleLinear().range([height, 0]);
const emit = defineEmits<{
(event: "selected-point", value: Point<unknown>): void;
}>();
const shape = d3
.area<Point<unknown>>()
.curve(d3.curveStep)
.x((d) => x(d.x))
.y0(height)
.y1((d) => y(d.y));
const path = computed(() => {
x.domain(d3.extent(data, (d) => d.x) as [number, number]);
y.domain(d3.extent(data, (d) => d.y) as [number, number]);
return shape(data) ?? "";
});
let lineX = $ref(0);
function onMove(e: MouseEvent) {
const { offsetX } = e;
const xValue = x.invert(offsetX);
const index = Math.round(xValue);
lineX = x(index);
const point = data[index];
emit("selected-point", point);
}
</script>
<style scoped>
:deep(.area) {
fill: var(--primary-color);
}
:deep(.line) {
stroke: var(--secondary-color);
stroke-width: 2;
display: none;
}
svg:hover :deep(.line) {
display: unset;
}
</style>

View File

@@ -0,0 +1,19 @@
<template>
<svg width="100%" height="8" class="zigzag">
<defs>
<pattern id="zigzag" x="0" y="0" width="30" height="8" patternUnits="userSpaceOnUse">
<line x1="0" y1="0" x2="15" y2="8" class="line" />
<line x1="15" y1="8" x2="30" y2="0" class="line" />
</pattern>
</defs>
<rect x="0" y="0" width="100%" height="100%" fill="url(#zigzag)"></rect>
</svg>
</template>
<style lang="scss" scoped>
.line {
stroke: var(--primary-color);
stroke-width: 1;
stroke-linecap: round;
}
</style>

View File

@@ -0,0 +1,206 @@
// Vitest Snapshot v1
exports[`<LogEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
<div class=\\"text column\\" data-v-a49e52d4=\\"\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render dates with 24 hour style 1`] = `
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42</time></div>
<div class=\\"text column\\" data-v-a49e52d4=\\"\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages 1`] = `
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
<div class=\\"text column\\" data-v-a49e52d4=\\"\\">This is a message.</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with color 1`] = `
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
<div class=\\"text column\\" data-v-a49e52d4=\\"\\"><span style=\\"color:#000\\">black<span style=\\"color:#AAA\\">white</span></span></div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with filter 1`] = `
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
<li data-key=\\"2\\" class=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"line-options\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2\\" data-v-2e92daca=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
<div class=\\"text column\\" data-v-a49e52d4=\\"\\"><mark>test</mark> bar</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with html entities 1`] = `
"<ul class=\\"events medium\\" data-v-2e92daca=\\"\\">
<li data-key=\\"1\\" class=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"line-options\\" data-v-2e92daca=\\"\\" style=\\"display: none;\\">
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-2e92daca=\\"\\">
<div class=\\"level is-justify-content-start\\" data-v-2e92daca=\\"\\">
<div class=\\"level-left\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\"><svg viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-2e92daca=\\"\\">
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
</svg></div>
</div>
<div class=\\"level-right\\" data-v-2e92daca=\\"\\">
<div class=\\"level-item\\" data-v-2e92daca=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"columns is-1 is-variable\\" visible-keys=\\"\\" data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\">
<div class=\\"column is-narrow\\" data-v-a49e52d4=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"date\\" data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\">06/12/2019 10:55:42 AM</time></div>
<div class=\\"text column\\" data-v-a49e52d4=\\"\\">&lt;test&gt;foo bar&lt;/test&gt;</div>
</div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > renders correctly 1`] = `
"<div class=\\"infinte-loader\\" data-v-1cd63c6e=\\"\\">
<div class=\\"spinner\\" data-v-1cd63c6e=\\"\\" style=\\"display: none;\\">
<div class=\\"bounce1\\" data-v-1cd63c6e=\\"\\"></div>
<div class=\\"bounce2\\" data-v-1cd63c6e=\\"\\"></div>
<div class=\\"bounce3\\" data-v-1cd63c6e=\\"\\"></div>
</div>
</div>
<ul class=\\"events medium\\" data-v-2e92daca=\\"\\"></ul>"
`;
exports[`<LogEventSource /> > should parse messages 1`] = `
SimpleLogEntry {
"_message": "This is a message.",
"date": 2019-06-12T10:55:42.459Z,
"id": 1,
}
`;

View File

@@ -1,26 +0,0 @@
<template>
<log-event-source ref="source" :id="id" #default="{ messages }" @loading-more="emit('loading-more', $event)">
<log-viewer :messages="messages"></log-viewer>
</log-event-source>
</template>
<script lang="ts" setup>
import LogViewer from "./LogViewer.vue";
import { ref } from "vue";
defineProps({
id: {
type: String,
required: true,
},
});
const emit = defineEmits(["loading-more"]);
const source = ref<InstanceType<typeof LogViewer>>();
function clear() {
source.value?.clear();
}
defineExpose({
clear,
});
</script>

View File

@@ -2,7 +2,7 @@
<aside>
<div class="columns is-marginless is-gapless is-mobile is-vcentered">
<div class="column is-narrow">
<router-link :to="{ name: 'default' }">
<router-link :to="{ name: 'index' }">
<svg class="logo">
<use href="#logo"></use>
</svg>
@@ -24,10 +24,44 @@
</div>
</div>
<p class="menu-label is-hidden-mobile" :class="{ 'is-active': showNav }">Containers</p>
<div class="menu-label level is-mobile is-hidden-mobile" :class="{ 'is-active': showNav }">
<div class="level-item has-text-centered">
<div>
<button class="button is-small is-rounded" @click="$emit('search')" :title="$t('tooltip.search')">
<span class="icon">
<mdi-light-magnify />
</span>
</button>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<router-link :to="{ name: 'settings' }" active-class="is-active" class="button is-small is-rounded">
<span class="icon">
<mdi-light-cog />
</span>
</router-link>
</div>
</div>
<div class="level-item has-text-centered" v-if="secured">
<div>
<a class="button is-small is-rounded" :href="`${base}/logout`" :title="$t('button.logout')">
<span class="icon">
<mdi-light-logout />
</span>
</a>
</div>
</div>
</div>
<p class="menu-label is-hidden-mobile" :class="{ 'is-active': showNav }">{{ $t("label.containers") }}</p>
<ul class="menu-list is-hidden-mobile" :class="{ 'is-active': showNav }">
<li v-for="item in visibleContainers" :key="item.id">
<router-link :to="{ name: 'container', params: { id: item.id } }" active-class="is-active" :title="item.name">
<router-link
:to="{ name: 'container-id', params: { id: item.id } }"
active-class="is-active"
:title="item.name"
>
<div class="is-ellipsis">
{{ item.name }}
</div>
@@ -38,19 +72,15 @@
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
import { useContainerStore } from "@/stores/container";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
const { base, secured } = config;
const store = useContainerStore();
const route = useRoute();
const { visibleContainers, allContainersById } = storeToRefs(store);
const showNav = ref(false);
let showNav = $ref(false);
watch(route, () => {
showNav.value = false;
showNav = false;
});
</script>
<style scoped lang="scss">
@@ -64,6 +94,10 @@ aside {
max-height: 100vh;
overflow: auto;
.level.is-hidden-mobile.is-active {
display: flex !important;
}
.menu-label {
margin-top: 1em;
}

View File

@@ -3,20 +3,15 @@
</template>
<script lang="ts" setup>
import { useIntervalFn } from "@vueuse/core";
import formatDistance from "date-fns/formatDistance";
import { PropType, ref } from "vue";
const props = defineProps({
date: {
required: true,
type: Object as PropType<Date>,
},
});
const { date } = defineProps<{
date: Date;
}>();
const text = ref<string>();
function updateFromNow() {
text.value = formatDistance(props.date, new Date(), {
text.value = formatDistance(date, new Date(), {
addSuffix: true,
});
}

View File

@@ -1,44 +1,21 @@
<template>
<time :datetime="date.toISOString()">{{ relativeTime(date, locale) }}</time>
<time :datetime="date.toISOString()">{{ format(date) }}</time>
</template>
<script lang="ts">
const use24Hr =
new Intl.DateTimeFormat(undefined, {
hour: "numeric",
})
.formatToParts(new Date(2020, 0, 1, 13))
.find((part) => part.type === "hour")?.value.length === 2;
const auto = use24Hr ? enGB : enUS;
const styles = { auto, 12: enUS, 24: enGB };
</script>
<script lang="ts" setup>
import { formatRelative } from "date-fns";
import { hourStyle } from "@/composables/settings";
import enGB from "date-fns/locale/en-GB";
import enUS from "date-fns/locale/en-US";
import { computed, PropType } from "vue";
defineProps({
date: {
required: true,
type: Object as PropType<Date>,
},
});
defineProps<{
date: Date;
}>();
const locale = computed(() => {
const locale = styles[hourStyle.value];
const oldFormatter = locale.formatRelative as (d: Date | number) => string;
return {
...locale,
formatRelative(date: Date | number) {
return oldFormatter(date) + "p";
},
};
});
const dateFormatter = new Intl.DateTimeFormat(undefined, { day: "2-digit", month: "2-digit", year: "numeric" });
const use12Hour = $computed(() => ({ auto: undefined, "12": true, "24": false }[hourStyle.value]));
const timeFormatter = $computed(
() => new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit", second: "2-digit", hour12: use12Hour })
);
function relativeTime(date: Date, locale: Locale) {
return formatRelative(date, new Date(), { locale });
function format(date: Date) {
const dateStr = dateFormatter.format(date);
const timeStr = timeFormatter.format(date);
return `${dateStr} ${timeStr}`;
}
</script>

View File

@@ -18,21 +18,10 @@
</template>
<script lang="ts" setup>
import { useContainerStore } from "@/stores/container";
import { useScroll } from "@vueuse/core";
import { storeToRefs } from "pinia";
import { onMounted, ref, watch, watchPostEffect } from "vue";
const props = defineProps({
indeterminate: {
default: false,
type: Boolean,
},
autoHide: {
default: true,
type: Boolean,
},
});
const { indeterminate = false, autoHide = false } = defineProps<{
indeterminate?: boolean;
autoHide?: boolean;
}>();
const scrollProgress = ref(0);
const animation = ref({ cancel: () => {} });
@@ -59,7 +48,7 @@ watchPostEffect(() => {
: (scrollElement.value as HTMLElement);
scrollProgress.value = scrollY.value / (parent.scrollHeight - parent.clientHeight);
animation.value.cancel();
if (props.autoHide && root.value) {
if (autoHide && root.value) {
animation.value = root.value.animate(
{ opacity: [1, 0] },
{

View File

@@ -16,7 +16,12 @@
<div class="is-scrollbar-notification">
<transition name="fade">
<button class="button" :class="hasMore ? 'has-more' : ''" @click="scrollToBottom('instant')" v-show="paused">
<button
class="button has-boxshadow"
:class="hasMore ? 'has-more' : ''"
@click="scrollToBottom()"
v-show="paused"
>
<mdi-light-chevron-double-down />
</button>
</transition>
@@ -24,61 +29,46 @@
</section>
</template>
<script lang="ts">
export default {
props: {
scrollable: {
type: Boolean,
default: true,
},
},
<script lang="ts" setup>
const { scrollable = false } = defineProps<{ scrollable?: boolean }>();
name: "ScrollableView",
data() {
return {
paused: false,
hasMore: false,
loading: false,
mutationObserver: null,
intersectionObserver: null,
};
},
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 });
let paused = $ref(false);
let hasMore = $ref(false);
let loading = $ref(false);
const scrollObserver = ref<HTMLElement>();
const scrollableContent = ref<HTMLElement>();
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;
},
},
};
provide("scrollingPaused", $$(paused));
const mutationObserver = new MutationObserver((e) => {
if (!paused) {
scrollToBottom();
} else {
const record = e[e.length - 1];
if (record.target.children[record.target.children.length - 1] == record.addedNodes[record.addedNodes.length - 1]) {
hasMore = true;
}
}
});
const intersectionObserver = new IntersectionObserver((entries) => (paused = entries[0].intersectionRatio == 0), {
threshold: [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 = false;
}
function setLoading(value: boolean) {
loading = value;
}
</script>
<style scoped lang="scss">
section {
@@ -124,18 +114,18 @@ section {
button {
position: fixed;
bottom: 30px;
background-color: var(--secondary-color);
transition: background-color 1s ease-out;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
background-color: var(--primary-color);
transition: background-color 0.24s ease-out;
border: none !important;
color: #222;
color: #eee;
&.has-more {
background-color: var(--primary-color);
background-color: var(--secondary-color);
animation-name: bounce;
animation-duration: 1000ms;
animation-fill-mode: both;
color: #fff;
color: #222;
}
}
}

View File

@@ -22,30 +22,18 @@
</template>
<script lang="ts" setup>
import hotkeys from "hotkeys-js";
import { search } from "@/composables/settings";
import { useSearchFilter } from "@/composables/search";
import { ref, nextTick, onMounted, onUnmounted } from "vue";
const input = ref<HTMLInputElement>();
const { searchFilter, showSearch, resetSearch } = useSearchFilter();
onMounted(() => {
hotkeys("command+f, ctrl+f", (event, handler) => {
onKeyStroke("f", (e) => {
if (e.ctrlKey || e.metaKey) {
showSearch.value = true;
nextTick(() => input.value?.focus() || input.value?.select());
event.preventDefault();
});
hotkeys("esc", () => resetSearch());
e.preventDefault();
}
});
onUnmounted(() => {
searchFilter.value = "";
showSearch.value = false;
hotkeys.unbind("command+f, ctrl+f");
hotkeys.unbind("esc");
});
onUnmounted(() => resetSearch());
</script>
<style lang="scss" scoped>

View File

@@ -2,31 +2,44 @@
<aside>
<div class="columns is-marginless">
<div class="column is-paddingless">
<router-link :to="{ name: 'default' }">
<router-link :to="{ name: 'index' }">
<svg class="logo">
<use href="#logo"></use>
</svg>
</router-link>
</div>
<div class="column is-narrow has-text-right px-1">
<button class="button is-rounded" @click="$emit('search')" title="Search containers (⌘ + k, ⌃k)">
</div>
<div class="columns is-marginless">
<div class="column is-narrow py-0 pl-0 pr-1">
<button class="button is-rounded is-small" @click="$emit('search')" :title="$t('tooltip.search')">
<span class="icon">
<mdi-light-magnify />
</span>
</button>
</div>
<div class="column is-narrow has-text-right px-0">
<router-link :to="{ name: 'settings' }" active-class="is-active" class="button is-rounded">
<div class="column is-narrow py-0" :class="secured ? 'pl-0 pr-1' : 'px-0'">
<router-link :to="{ name: 'settings' }" active-class="is-active" class="button is-rounded is-small">
<span class="icon">
<mdi-light-cog />
</span>
</router-link>
</div>
<div class="column is-narrow py-0 px-0" v-if="secured">
<a class="button is-rounded is-small" :href="`${base}/logout`" :title="$t('button.logout')">
<span class="icon">
<mdi-light-logout />
</span>
</a>
</div>
</div>
<p class="menu-label is-hidden-mobile">Containers</p>
<p class="menu-label is-hidden-mobile">{{ $t("label.containers") }}</p>
<ul class="menu-list is-hidden-mobile" v-if="ready">
<li v-for="item in visibleContainers" :key="item.id" :class="item.state">
<router-link :to="{ name: 'container', params: { id: item.id } }" active-class="is-active" :title="item.name">
<router-link
:to="{ name: 'container-id', params: { id: item.id } }"
active-class="is-active"
:title="item.name"
>
<div class="container is-flex is-align-items-center">
<div class="is-flex-grow-1 is-ellipsis">
{{ item.name }}
@@ -36,7 +49,7 @@
class="icon is-small"
@click.stop.prevent="store.appendActiveContainer(item)"
v-show="!activeContainersById[item.id]"
title="Pin as column"
:title="$t('tooltip.pin-column')"
>
<cil-columns />
</span>
@@ -46,17 +59,15 @@
</li>
</ul>
<ul class="menu-list is-hidden-mobile loading" v-else>
<li v-for="index in 7" class="my-4"><o-skeleton animated size="large"></o-skeleton></li>
<li v-for="index in 7" class="my-4"><o-skeleton animated size="large" :key="index"></o-skeleton></li>
</ul>
</aside>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { storeToRefs } from "pinia";
import { useContainerStore } from "@/stores/container";
import type { Container } from "@/types/Container";
const { base, secured } = config;
const store = useContainerStore();
const { activeContainers, visibleContainers, ready } = storeToRefs(store);

View File

@@ -1,201 +0,0 @@
// Vitest Snapshot v1
exports[`<LogEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
<li data-key=\\"2019-06-12T23:55:42.459034602Z\\" 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=\\"#2019-06-12T23:55:42.459034602Z\\" 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-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T23:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 11:55:42 PM</time></span><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-cce5b553=\\"\\">
<li data-key=\\"2019-06-12T23:55:42.459034602Z\\" 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=\\"#2019-06-12T23:55:42.459034602Z\\" 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-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</div>
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T23:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 23:55:42</time></span><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-cce5b553=\\"\\">
<li data-key=\\"2019-06-12T10:55:42.459034602Z\\" 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=\\"#2019-06-12T10:55:42.459034602Z\\" 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-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</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><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-cce5b553=\\"\\">
<li data-key=\\"2019-06-12T10:55:42.459034602Z\\" 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=\\"#2019-06-12T10:55:42.459034602Z\\" 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-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</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><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-cce5b553=\\"\\">
<li data-key=\\"2019-06-12T10:55:42.459034602Z\\" 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=\\"#2019-06-12T10:55:42.459034602Z\\" 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-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</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><span class=\\"text\\" data-v-cce5b553=\\"\\">This is a <mark>test</mark> &lt;hi&gt;&lt;/hi&gt;</span></div>
</li>
</ul>"
`;
exports[`<LogEventSource /> > render html correctly > should render messages with html entities 1`] = `
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
<li data-key=\\"2019-06-12T10:55:42.459034602Z\\" 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=\\"#2019-06-12T10:55:42.459034602Z\\" 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-cce5b553=\\"\\">
<div class=\\"level-item\\" data-v-cce5b553=\\"\\">Jump to Context</div>
</div>
</div>
</a></div>
</div>
</div>
</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><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-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-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.\\"",
}
`;

View File

@@ -0,0 +1,125 @@
import { type ComputedRef, type Ref } from "vue";
import debounce from "lodash.debounce";
import {
type LogEvent,
type JSONObject,
LogEntry,
asLogEntry,
DockerEventLogEntry,
SkippedLogsEntry,
} from "@/models/LogEntry";
import { Container } from "@/models/Container";
function parseMessage(data: string): LogEntry<string | JSONObject> {
const e = JSON.parse(data) as LogEvent;
return asLogEntry(e);
}
export function useLogStream(container: ComputedRef<Container>) {
let messages: LogEntry<string | JSONObject>[] = $ref([]);
let buffer: LogEntry<string | JSONObject>[] = $ref([]);
const scrollingPaused = $ref(inject("scrollingPaused") as Ref<boolean>);
function flushNow() {
if (messages.length > config.maxLogs) {
if (scrollingPaused) {
console.log("Skipping ", buffer.length, " log items");
if (messages.at(-1) instanceof SkippedLogsEntry) {
const lastEvent = messages.at(-1) as SkippedLogsEntry;
const lastItem = buffer.at(-1) as LogEntry<string | JSONObject>;
lastEvent.addSkippedEntries(buffer.length, lastItem);
} else {
const firstItem = buffer.at(0) as LogEntry<string | JSONObject>;
const lastItem = buffer.at(-1) as LogEntry<string | JSONObject>;
messages.push(new SkippedLogsEntry(new Date(), buffer.length, firstItem, lastItem));
}
buffer = [];
} else {
messages.push(...buffer);
buffer = [];
messages = messages.slice(-config.maxLogs);
}
} else {
messages.push(...buffer);
buffer = [];
}
}
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 = [];
buffer = [];
lastEventId = "";
}
es = new EventSource(`${config.base}/api/logs/stream?id=${container.value.id}&lastEventId=${lastEventId}`);
es.addEventListener("container-stopped", () => {
es?.close();
es = null;
buffer.push(new DockerEventLogEntry("Container stopped", new Date(), "container-stopped"));
flushBuffer();
flushBuffer.flush();
});
es.addEventListener("error", (e) => console.error("EventSource failed: " + JSON.stringify(e)));
es.onmessage = (e) => {
lastEventId = e.lastEventId;
if (e.data) {
buffer.push(parseMessage(e.data));
flushBuffer();
}
};
}
async function loadOlderLogs({ beforeLoading, afterLoading } = { beforeLoading: () => {}, afterLoading: () => {} }) {
if (messages.length < 300) return;
beforeLoading();
const to = messages[0].date;
const last = messages[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.unshift(...newMessages);
}
afterLoading();
}
watch(
() => container.value.state,
(newValue, oldValue) => {
console.log("LogEventSource: container changed", newValue, oldValue);
if (newValue == "running" && newValue != oldValue) {
buffer.push(new DockerEventLogEntry("Container started", new Date(), "container-started"));
connect({ clear: false });
}
}
);
onUnmounted(() => {
if (es) {
es.close();
}
});
watch(
() => container.value.id,
() => connect(),
{ immediate: true }
);
return { ...$$({ messages }), loadOlderLogs };
}

View File

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

View File

@@ -1,21 +1,41 @@
import { ref, computed, Ref } from "vue";
import { type Ref } from "vue";
import { type LogEntry, type JSONObject, SimpleLogEntry, ComplexLogEntry } from "@/models/LogEntry";
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<LogEntry<string | JSONObject>[]>) {
return computed(() => {
if (searchFilter && searchFilter.value) {
if (debouncedSearchFilter.value && showSearch.value) {
try {
return messages.value.filter((d) => d.message.match(regex.value));
return messages.value.filter((d) => {
if (d instanceof SimpleLogEntry) {
return regex.value.test(d.message);
} else if (d instanceof ComplexLogEntry) {
return matchRecord(d.message, regex.value);
}
return false;
});
} catch (e) {
if (e instanceof SyntaxError) {
console.info(`Ignoring SyntaxError from search.`, e);
@@ -29,11 +49,17 @@ export function useSearchFilter() {
});
}
function markSearch(log: string) {
if (searchFilter && searchFilter.value) {
return log.replace(regex.value, `<mark>$&</mark>`);
function markSearch(log: { toString(): string }): string;
function markSearch(log: string[]): string[];
function markSearch(log: { toString(): 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

@@ -1,7 +1,4 @@
import { useStorage } from "@vueuse/core";
import { computed } from "vue";
export const DOZZLE_SETTINGS_KEY = "DOZZLE_SETTINGS";
const DOZZLE_SETTINGS_KEY = "DOZZLE_SETTINGS";
export const DEFAULT_SETTINGS: {
search: boolean;
@@ -13,6 +10,7 @@ export const DEFAULT_SETTINGS: {
lightTheme: "auto" | "dark" | "light";
hourStyle: "auto" | "24" | "12";
softWrap: boolean;
collapseNav: boolean;
} = {
search: true,
size: "medium",
@@ -23,51 +21,71 @@ export const DEFAULT_SETTINGS: {
lightTheme: "auto",
hourStyle: "auto",
softWrap: true,
collapseNav: false,
};
export const settings = useStorage(DOZZLE_SETTINGS_KEY, DEFAULT_SETTINGS);
settings.value = {...DEFAULT_SETTINGS, ...settings.value};
const settings = useStorage(DOZZLE_SETTINGS_KEY, DEFAULT_SETTINGS);
settings.value = { ...DEFAULT_SETTINGS, ...settings.value };
export const search = computed({
const search = computed({
get: () => settings.value.search,
set: (value) => (settings.value.search = value),
});
export const size = computed({
const size = computed({
get: () => settings.value.size,
set: (value) => (settings.value.size = value),
});
export const menuWidth = computed({
const menuWidth = computed({
get: () => settings.value.menuWidth,
set: (value) => (settings.value.menuWidth = value),
});
export const smallerScrollbars = computed({
const smallerScrollbars = computed({
get: () => settings.value.smallerScrollbars,
set: (value) => (settings.value.smallerScrollbars = value),
});
export const showTimestamp = computed({
const showTimestamp = computed({
get: () => settings.value.showTimestamp,
set: (value) => (settings.value.showTimestamp = value),
});
export const showAllContainers = computed({
const showAllContainers = computed({
get: () => settings.value.showAllContainers,
set: (value) => (settings.value.showAllContainers = value),
});
export const lightTheme = computed({
const lightTheme = computed({
get: () => settings.value.lightTheme,
set: (value) => (settings.value.lightTheme = value),
});
export const hourStyle = computed({
const hourStyle = computed({
get: () => settings.value.hourStyle,
set: (value) => (settings.value.hourStyle = value),
});
export const softWrap = computed({
const softWrap = computed({
get: () => settings.value.softWrap,
set: (value) => (settings.value.softWrap = value),
});
const collapseNav = computed({
get: () => settings.value.collapseNav,
set: (value) => (settings.value.collapseNav = value),
});
export {
collapseNav,
softWrap,
hourStyle,
lightTheme,
showAllContainers,
showTimestamp,
smallerScrollbars,
menuWidth,
size,
search,
settings
};

View File

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

View File

@@ -0,0 +1,18 @@
import { ComplexLogEntry, type JSONObject, type LogEntry } from "@/models/LogEntry";
import type { ComputedRef, Ref } from "vue";
export function useVisibleFilter(visibleKeys: ComputedRef<Ref<string[][]>>) {
function filteredPayload(messages: Ref<LogEntry<string | JSONObject>[]>) {
return computed(() => {
return messages.value.map((d) => {
if (d instanceof ComplexLogEntry) {
return ComplexLogEntry.fromLogEvent(d, visibleKeys.value);
} else {
return d;
}
});
});
}
return { filteredPayload };
}

125
assets/layouts/default.vue Normal file
View File

@@ -0,0 +1,125 @@
<template>
<main v-if="!authorizationNeeded">
<mobile-menu v-if="isMobile" @search="showFuzzySearch"></mobile-menu>
<splitpanes @resized="onResized($event)">
<pane min-size="10" :size="menuWidth" v-if="!isMobile && !collapseNav">
<side-menu @search="showFuzzySearch"></side-menu>
</pane>
<pane min-size="10">
<splitpanes>
<pane class="has-min-height router-view">
<router-view></router-view>
</pane>
<template v-if="!isMobile">
<pane v-for="other in activeContainers" :key="other.id">
<log-container
:id="other.id"
show-title
scrollable
closable
@close="containerStore.removeActiveContainer(other)"
></log-container>
</pane>
</template>
</splitpanes>
</pane>
</splitpanes>
<button
@click="collapse"
class="button is-small is-rounded"
:class="{ collapsed: collapseNav }"
id="hide-nav"
v-if="!isMobile"
>
<span class="icon ml-2" v-if="collapseNav">
<mdi-light-chevron-right />
</span>
<span class="icon" v-else>
<mdi-light-chevron-left />
</span>
</button>
</main>
</template>
<script lang="ts" setup>
// @ts-ignore - splitpanes types are not available
import { Splitpanes, Pane } from "splitpanes";
import { useProgrammatic } from "@oruga-ui/oruga-next";
import FuzzySearchModal from "@/components/FuzzySearchModal.vue";
const { oruga } = useProgrammatic();
const { authorizationNeeded } = config;
const containerStore = useContainerStore();
const { activeContainers, visibleContainers } = storeToRefs(containerStore);
watchEffect(() => {
setTitle(`${visibleContainers.value.length} containers`);
});
onKeyStroke("k", (e) => {
if (e.ctrlKey || e.metaKey) {
showFuzzySearch();
e.preventDefault();
}
});
function showFuzzySearch() {
oruga.modal.open({
// parent: this,
component: FuzzySearchModal,
animation: "false",
width: 600,
active: true,
});
}
function collapse() {
collapseNav.value = !collapseNav.value;
}
function onResized(e: any) {
if (e.length == 2) {
menuWidth.value = e[0].size;
}
}
</script>
<style scoped lang="scss">
:deep(.splitpanes--vertical > .splitpanes__splitter) {
min-width: 3px;
background: var(--border-color);
&:hover {
background: var(--border-hover-color);
}
}
@media screen and (max-width: 768px) {
.router-view {
padding-top: 75px;
}
}
.button.has-no-border {
border-color: transparent !important;
}
.has-min-height {
min-height: 100vh;
}
#hide-nav {
position: fixed;
left: 10px;
bottom: 10px;
&.collapsed {
left: -40px;
width: 60px;
padding-left: 40px;
color: var(--text-strong-color);
background: var(--scheme-main);
&:hover {
left: -25px;
}
}
}
</style>

View File

@@ -0,0 +1,9 @@
<template>
<main>
<router-view></router-view>
</main>
</template>
<script lang="ts" setup></script>
<style scoped lang="scss"></style>

View File

@@ -1,68 +1,10 @@
import "./styles.scss";
import { createApp } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import { Autocomplete, Button, Dropdown, Switch, Radio, Skeleton, Field, Tooltip, Modal, Config } from "@oruga-ui/oruga-next";
import { bulmaConfig } from "@oruga-ui/theme-bulma";
import { createPinia } from "pinia";
import config from "./stores/config";
import { createApp, App as VueApp } from "vue";
import App from "./App.vue";
import { Container, Settings, Index, Show, ContainerNotFound, PageNotFound, Login } from "./pages";
const routes = [
{
path: "/",
component: Index,
name: "default",
},
{
path: "/container/:id",
component: Container,
name: "container",
props: true,
},
{
path: "/container/:pathMatch(.*)",
component: ContainerNotFound,
name: "container-not-found",
},
{
path: "/settings",
component: Settings,
name: "settings",
},
{
path: "/show",
component: Show,
name: "show",
},
{
path: "/login",
component: Login,
name: "login",
},
{
path: "/:pathMatch(.*)*",
component: PageNotFound,
name: "page-not-found",
},
];
const app = createApp(App);
Object.values(import.meta.glob<{ install: (app: VueApp) => void }>("./modules/*.ts", { eager: true })).forEach((i) =>
i.install?.(app)
);
const router = createRouter({
history: createWebHistory(`${config.base}/`),
routes,
});
createApp(App)
.use(router)
.use(createPinia())
.use(Autocomplete)
.use(Button)
.use(Dropdown)
.use(Switch)
.use(Tooltip)
.use(Modal)
.use(Radio)
.use(Field)
.use(Skeleton)
.use(Config, bulmaConfig)
.mount("#app");
app.mount("#app");

View File

@@ -0,0 +1,31 @@
import type { ContainerStat, ContainerState } from "@/types/Container";
import type { UseThrottledRefHistoryReturn } from "@vueuse/core";
import { Ref } from "vue";
type Stat = Omit<ContainerStat, "id">;
export class Container {
public stat: Ref<Stat>;
private readonly throttledStatHistory: UseThrottledRefHistoryReturn<Stat, Stat>;
constructor(
public readonly id: string,
public readonly created: number,
public readonly image: string,
public readonly name: string,
public readonly command: string,
public status: string,
public state: ContainerState
) {
this.stat = ref({ cpu: 0, memory: 0, memoryUsage: 0 });
this.throttledStatHistory = useThrottledRefHistory(this.stat, { capacity: 300, deep: true, throttle: 1000 });
}
public getStatHistory() {
return unref(this.throttledStatHistory.history);
}
public getLastStat() {
return unref(this.throttledStatHistory.last);
}
}

121
assets/models/LogEntry.ts Normal file
View File

@@ -0,0 +1,121 @@
import { Component, ComputedRef, Ref } from "vue";
import { flattenJSON, getDeep } from "@/utils";
import ComplexLogItem from "@/components/LogViewer/ComplexLogItem.vue";
import SimpleLogItem from "@/components/LogViewer/SimpleLogItem.vue";
import DockerEventLogItem from "@/components/LogViewer/DockerEventLogItem.vue";
import SkippedEntriesLogItem from "@/components/LogViewer/SkippedEntriesLogItem.vue";
export interface HasComponent {
getComponent(): Component;
}
export type JSONValue = string | number | boolean | JSONObject | Array<JSONValue>;
export type JSONObject = { [x: string]: JSONValue };
export interface LogEvent {
readonly m: string | JSONObject;
readonly ts: number;
readonly id: number;
}
export abstract class LogEntry<T extends string | JSONObject> implements HasComponent {
protected readonly _message: T;
constructor(message: T, public readonly id: number, public readonly date: Date) {
this._message = message;
}
public get message(): T {
return this._message;
}
abstract getComponent(): Component;
}
export class SimpleLogEntry extends LogEntry<string> {
getComponent(): Component {
return SimpleLogItem;
}
}
export class ComplexLogEntry extends LogEntry<JSONObject> {
private readonly filteredMessage: ComputedRef<JSONObject>;
constructor(message: JSONObject, id: number, date: Date, visibleKeys?: Ref<string[][]>) {
super(message, id, date);
if (visibleKeys) {
this.filteredMessage = computed(() => {
if (!visibleKeys.value.length) {
return flattenJSON(message);
} else {
return visibleKeys.value.reduce((acc, attr) => ({ ...acc, [attr.join(".")]: getDeep(message, attr) }), {});
}
});
} else {
this.filteredMessage = computed(() => flattenJSON(message));
}
}
getComponent(): Component {
return ComplexLogItem;
}
public get message(): JSONObject {
return this.filteredMessage.value;
}
public get unfilteredMessage(): JSONObject {
return this._message;
}
static fromLogEvent(event: ComplexLogEntry, visibleKeys: Ref<string[][]>): ComplexLogEntry {
return new ComplexLogEntry(event._message, event.id, event.date, visibleKeys);
}
}
export class DockerEventLogEntry extends LogEntry<string> {
constructor(message: string, date: Date, public readonly event: string) {
super(message, date.getTime(), date);
}
getComponent(): Component {
return DockerEventLogItem;
}
}
export class SkippedLogsEntry extends LogEntry<string> {
private _totalSkipped = 0;
private lastSkipped: LogEntry<string | JSONObject>;
constructor(
date: Date,
totalSkipped: number,
public readonly firstSkipped: LogEntry<string | JSONObject>,
lastSkipped: LogEntry<string | JSONObject>
) {
super("", date.getTime(), date);
this._totalSkipped = totalSkipped;
this.lastSkipped = lastSkipped;
}
getComponent(): Component {
return SkippedEntriesLogItem;
}
public get message(): string {
return `Skipped ${this.totalSkipped} entries`;
}
public addSkippedEntries(totalSkipped: number, lastItem: LogEntry<string | JSONObject>) {
this._totalSkipped += totalSkipped;
this.lastSkipped = lastItem;
}
public get totalSkipped(): number {
return this._totalSkipped;
}
}
export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
if (typeof event.m === "string") {
return new SimpleLogEntry(event.m, event.id, new Date(event.ts));
} else {
return new ComplexLogEntry(event.m, event.id, new Date(event.ts));
}
}

28
assets/modules/bulma.ts Normal file
View File

@@ -0,0 +1,28 @@
import { type App } from "vue";
import {
Autocomplete,
Button,
Dropdown,
Switch,
Radio,
Skeleton,
Field,
Tooltip,
Modal,
Config,
} from "@oruga-ui/oruga-next";
import { bulmaConfig } from "@oruga-ui/theme-bulma";
export const install = (app: App) => {
app
.use(Autocomplete)
.use(Button)
.use(Dropdown)
.use(Switch)
.use(Tooltip)
.use(Modal)
.use(Radio)
.use(Field)
.use(Skeleton)
.use(Config, bulmaConfig);
};

20
assets/modules/i18n.ts Normal file
View File

@@ -0,0 +1,20 @@
import { type App } from "vue";
import { createI18n } from "vue-i18n";
export const install = (app: App) => {
const messages = Object.fromEntries(
Object.entries(import.meta.glob<{ default: any }>("../../locales/*.y(a)?ml", { eager: true })).map(
([key, value]) => {
const yaml = key.endsWith(".yaml");
return [key.slice(14, yaml ? -5 : -4), value.default];
}
)
);
const i18n = createI18n({
legacy: false,
locale: navigator.language.slice(0, 2),
fallbackLocale: "en",
messages,
});
app.use(i18n);
};

8
assets/modules/pinia.ts Normal file
View File

@@ -0,0 +1,8 @@
import { type App } from "vue";
import { createPinia } from "pinia";
export const install = (app:App) => {
const pinia = createPinia();
app.use(pinia);
};

16
assets/modules/router.ts Normal file
View File

@@ -0,0 +1,16 @@
import { type App } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import pages from "~pages";
import { setupLayouts } from "virtual:generated-layouts";
import config from "@/stores/config";
export const install = (app: App) => {
const routes = setupLayouts(pages);
const router = createRouter({
history: createWebHistory(`${config.base}/`),
routes,
});
app.use(router);
};

View File

@@ -1,35 +0,0 @@
<template>
<search></search>
<log-container :id="id" show-title :scrollable="activeContainers.length > 0"> </log-container>
</template>
<script lang="ts" setup>
import { onMounted, toRefs, watchEffect } from "vue";
import Search from "@/components/Search.vue";
import LogContainer from "@/components/LogContainer.vue";
import { setTitle } from "@/composables/title";
import { useContainerStore } from "@/stores/container";
import { storeToRefs } from "pinia";
const store = useContainerStore();
const props = defineProps({
id: {
type: String,
required: true,
},
});
const { id } = toRefs(props);
const currentContainer = store.currentContainer(id);
const { activeContainers } = storeToRefs(store);
setTitle("loading");
onMounted(() => {
setTitle(currentContainer.value?.name);
});
watchEffect(() => setTitle(currentContainer.value?.name));
</script>

View File

@@ -1,23 +0,0 @@
<template>
<div class="hero is-halfheight">
<div class="hero-body">
<div class="container has-text-centered">
<h1 class="title">
Container not found.
<small class="subtitle">It may have been removed.</small>
</h1>
</div>
</div>
</div>
</template>
<script lang="ts">
import { setTitle } from "@/composables/title";
export default {
name: "ContainerNotFound",
setup() {
setTitle("Container not found");
},
};
</script>

View File

@@ -4,19 +4,14 @@
<div class="container has-text-centered">
<h1 class="title">
404.
<small class="subtitle">This page does not exist.</small>
<small class="subtitle">{{ $t("error.page-not-found") }}</small>
</h1>
</div>
</div>
</div>
</template>
<script lang="ts">
import { setTitle } from "@/composables/title";
export default {
name: "PageNotFound",
setup() {
setTitle("Page not found");
},
};
<script lang="ts" setup>
const { t } = useI18n();
setTitle(t("title.page-not-found"));
</script>

View File

@@ -0,0 +1,20 @@
<template>
<search></search>
<log-container :id="id" show-title :scrollable="activeContainers.length > 0"> </log-container>
</template>
<script lang="ts" setup>
const store = useContainerStore();
const { id } = defineProps<{ id: string }>();
const currentContainer = store.currentContainer($$(id));
const { activeContainers } = storeToRefs(store);
setTitle("loading");
onMounted(() => {
setTitle(currentContainer.value?.name);
});
watchEffect(() => setTitle(currentContainer.value?.name));
</script>

View File

@@ -1,7 +0,0 @@
export { default as Index } from "./Index.vue";
export { default as ContainerNotFound } from "./ContainerNotFound.vue";
export { default as Show } from "./Show.vue";
export { default as Container } from "./Container.vue";
export { default as Settings } from "./Settings.vue";
export { default as PageNotFound } from "./PageNotFound.vue";
export { default as Login } from "./Login.vue";

View File

@@ -1,64 +1,50 @@
<template>
<div>
<section class="hero is-small mt-4">
<div class="hero-body">
<div class="container">
<div class="columns">
<div class="column">
<h1 class="title">Hello, there!</h1>
</div>
<div class="column is-narrow" v-if="secured">
<a class="button is-primary is-small" :href="`${base}/logout`">Logout</a>
</div>
</div>
</div>
</div>
</section>
<section class="level section">
<section class="level section pb-0-is-mobile">
<div class="level-item has-text-centered">
<div>
<p class="title">{{ containers.length }}</p>
<p class="heading">Total Containers</p>
<p class="heading">{{ $t("label.total-containers") }}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title">{{ runningContainers.length }}</p>
<p class="heading">Running</p>
<p class="heading">{{ $t("label.running") }}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title" data-ci-skip>{{ totalCpu }}%</p>
<p class="heading">Total CPU Usage</p>
<p class="heading">{{ $t("label.total-cpu-usage") }}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title" data-ci-skip>{{ formatBytes(totalMem) }}</p>
<p class="heading">Total Mem Usage</p>
<p class="heading">{{ $t("label.total-mem-usage") }}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title">{{ version }}</p>
<p class="heading">Dozzle Version</p>
<p class="heading">{{ $t("label.dozzle-version") }}</p>
</div>
</div>
</section>
<section class="columns is-centered section is-marginless">
<div class="column is-4">
<section class="columns is-centered section is-marginless pt-0-is-mobile">
<div class="column is-12-mobile is-6-tablet is-5-desktop is-4-fullhd">
<div class="panel">
<p class="panel-heading">Containers</p>
<p class="panel-heading">{{ $t("label.containers") }}</p>
<div class="panel-block">
<p class="control has-icons-left">
<input
class="input"
type="text"
placeholder="Search Containers"
v-model="search"
@keyup.esc="search = null"
:placeholder="$t('placeholder.search-containers')"
v-model="query"
@keyup.esc="query = ''"
@keyup.enter="onEnter()"
/>
<span class="icon is-left">
@@ -66,13 +52,13 @@
</span>
</p>
</div>
<p class="panel-tabs" v-if="!search">
<a :class="{ 'is-active': sort === 'running' }" @click="sort = 'running'">Running</a>
<a :class="{ 'is-active': sort === 'all' }" @click="sort = 'all'">All</a>
<p class="panel-tabs" v-if="query === ''">
<a :class="{ 'is-active': sort === 'running' }" @click="sort = 'running'">{{ $t("label.running") }}</a>
<a :class="{ 'is-active': sort === 'all' }" @click="sort = 'all'">{{ $t("label.all") }}</a>
</p>
<router-link
:to="{ name: 'container', params: { id: item.id, name: item.name } }"
v-for="item in results.slice(0, 10)"
:to="{ name: 'container-id', params: { id: item.id } }"
v-for="item in data.slice(0, 10)"
:key="item.id"
class="panel-block"
>
@@ -89,63 +75,60 @@
</template>
<script lang="ts" setup>
import { ref, computed } from "vue";
import { storeToRefs } from "pinia";
import { useRouter } from "vue-router";
import { useContainerStore } from "@/stores/container";
import { formatBytes } from "@/utils";
import fuzzysort from "fuzzysort";
import SearchIcon from "~icons/mdi-light/magnify";
import PastTime from "../components/PastTime.vue";
import config from "@/stores/config";
import { useIntervalFn } from "@vueuse/core";
import { useFuse } from "@vueuse/integrations/useFuse";
const { base, version, secured } = config;
const containerStore = useContainerStore();
const { containers } = storeToRefs(containerStore);
const router = useRouter();
const sort = ref("running");
const search = ref();
const sort = $ref("running");
const query = ref("");
const results = computed(() => {
if (search.value) {
return fuzzysort.go(search.value, containers.value, { key: "name" }).map((i) => i.obj);
const mostRecentContainers = $computed(() => [...containers.value].sort((a, b) => b.created - a.created));
const runningContainers = $computed(() => mostRecentContainers.filter((c) => c.state === "running"));
const { results } = useFuse(query, containers, {
fuseOptions: { keys: ["name"] },
matchAllWhenSearchEmpty: false,
});
const data = computed(() => {
if (results.value.length) {
return results.value.map(({ item }) => item);
}
switch (sort.value) {
switch (sort) {
case "all":
return mostRecentContainers.value;
return mostRecentContainers;
case "running":
return runningContainers.value;
return runningContainers;
default:
throw `Invalid sort order: ${sort.value}`;
throw `Invalid sort order: ${sort}`;
}
});
const mostRecentContainers = computed(() => [...containers.value].sort((a, b) => b.created - a.created));
const runningContainers = computed(() => mostRecentContainers.value.filter((c) => c.state === "running"));
const totalCpu = ref(0);
let totalCpu = $ref(0);
useIntervalFn(
() => {
totalCpu.value = runningContainers.value.reduce((acc, c) => acc + (c.stat?.cpu ?? 0), 0);
totalCpu = runningContainers.reduce((acc, c) => acc + (c.stat?.cpu ?? 0), 0);
},
1000,
{ immediate: true }
);
const totalMem = ref(0);
let totalMem = $ref(0);
useIntervalFn(
() => {
totalMem.value = runningContainers.value.reduce((acc, c) => acc + (c.stat?.memoryUsage ?? 0), 0);
totalMem = runningContainers.reduce((acc, c) => acc + (c.stat?.memoryUsage ?? 0), 0);
},
1000,
{ immediate: true }
);
function onEnter() {
if (results.value.length == 1) {
const [item] = results.value;
router.push({ name: "container", params: { id: item.id, name: item.name } });
if (data.value.length > 0) {
const item = data.value[0];
router.push({ name: "container-id", params: { id: item.id } });
}
}
</script>
@@ -170,6 +153,16 @@ function onEnter() {
}
}
@media screen and (max-width: 768px) {
.pb-0-is-mobile {
padding-bottom: 0 !important;
}
.pt-0-is-mobile {
padding-top: 0 !important;
}
}
.icon {
padding: 10px 3px;
}

View File

@@ -8,7 +8,7 @@
<div class="card-content">
<form action="" method="post" @submit.prevent="onLogin" ref="form">
<div class="field">
<label class="label">Username</label>
<label class="label">{{ $t("label.username") }}</label>
<div class="control">
<input
class="input"
@@ -22,7 +22,7 @@
</div>
<div class="field">
<label class="label">Password</label>
<label class="label">{{ $t("label.password") }}</label>
<div class="control">
<input
class="input"
@@ -32,11 +32,11 @@
v-model="password"
/>
</div>
<p class="help is-danger" v-if="error">Username and password are not valid.</p>
<p class="help is-danger" v-if="error">{{ $t("error.invalid-auth") }}</p>
</div>
<div class="field is-grouped is-grouped-centered mt-5">
<p class="control">
<button class="button is-primary" type="submit">Login</button>
<button class="button is-primary" type="submit">{{ $t("button.login") }}</button>
</p>
</div>
</form>
@@ -49,35 +49,31 @@
</div>
</template>
<script lang="ts">
import config from "@/stores/config";
import { setTitle } from "@/composables/title";
export default {
name: "Login",
data() {
return {
username: null,
password: null,
error: false,
};
},
setup() {
setTitle("Authentication Required");
},
methods: {
async onLogin() {
const response = await fetch(`${config.base}/api/validateCredentials`, {
body: new FormData(this.$refs.form),
method: "post",
});
<script lang="ts" setup>
const { t } = useI18n();
if (response.status == 200) {
this.error = false;
window.location.href = `${config.base}/`;
} else {
this.error = true;
}
},
},
};
setTitle(t("title.login"));
let error = $ref(false);
let username = $ref("");
let password = $ref("");
let form: HTMLFormElement = $ref();
async function onLogin() {
const response = await fetch(`${config.base}/api/validateCredentials`, {
body: new FormData(form),
method: "post",
});
if (response.status == 200) {
error = false;
window.location.href = `${config.base}/`;
} else {
error = true;
}
}
</script>
<route lang="yaml">
meta:
layout: splash
</route>

View File

@@ -2,35 +2,32 @@
<div>
<section class="section">
<div class="has-underline">
<h2 class="title is-4">About</h2>
<h2 class="title is-4">{{ $t("settings.about") }}</h2>
</div>
<div>
You are using Dozzle <i>{{ currentVersion }}</i
>.
<span v-if="hasUpdate">
New version is available! Update to
<a :href="nextRelease.html_url" class="next-release" target="_blank" rel="noreferrer noopener">
{{ nextRelease.name }}</a
>.
</span>
<span v-html="$t('settings.using-version', { version: currentVersion })"></span>
<div
v-if="hasUpdate"
v-html="$t('settings.update-available', { nextVersion: nextRelease.name, href: nextRelease.html_url })"
></div>
</div>
</section>
<section class="section">
<div class="has-underline">
<h2 class="title is-4">Display</h2>
<h2 class="title is-4">{{ $t("settings.display") }}</h2>
</div>
<div class="item">
<o-switch v-model="smallerScrollbars"> Use smaller scrollbars </o-switch>
<o-switch v-model="smallerScrollbars"> {{ $t("settings.small-scrollbars") }} </o-switch>
</div>
<div class="item">
<o-switch v-model="showTimestamp"> Show timestamps </o-switch>
<o-switch v-model="showTimestamp"> {{ $t("settings.show-timesamps") }} </o-switch>
</div>
<div class="item">
<o-switch v-model="softWrap"> Soft wrap lines</o-switch>
<o-switch v-model="softWrap"> {{ $t("settings.soft-wrap") }}</o-switch>
</div>
<div class="item">
@@ -54,7 +51,7 @@
</o-field>
</div>
<div class="column">
By default, Dozzle will use your browser's locale to format time. You can force to 12 or 24 hour style.
{{ $t("settings.12-24-format") }}
</div>
</div>
</div>
@@ -83,7 +80,7 @@
</o-dropdown>
</o-field>
</div>
<div class="column">Font size to use for logs</div>
<div class="column">{{ $t("settings.font-size") }}</div>
</div>
</div>
<div class="item">
@@ -111,33 +108,30 @@
</o-dropdown>
</o-field>
</div>
<div class="column">Color scheme</div>
<div class="column">{{ $t("settings.color-scheme") }}</div>
</div>
</div>
</section>
<section class="section">
<div class="has-underline">
<h2 class="title is-4">Options</h2>
<h2 class="title is-4">{{ $t("settings.options") }}</h2>
</div>
<div class="item">
<o-switch v-model="search">
Enable searching with Dozzle using <code>command+f</code> or <code>ctrl+f</code>
<span v-html="$t('settings.search')"></span>
</o-switch>
</div>
<div class="item">
<o-switch v-model="showAllContainers"> Show stopped containers </o-switch>
<o-switch v-model="showAllContainers"> {{ $t("settings.show-stopped-containers") }} </o-switch>
</div>
</section>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import gt from "semver/functions/gt";
import config from "@/stores/config";
import { setTitle } from "@/composables/title";
import {
search,
lightTheme,
@@ -149,22 +143,28 @@ import {
softWrap,
} from "@/composables/settings";
setTitle("Settings");
const { t } = useI18n();
const currentVersion = config.version;
const nextRelease = ref({ html_url: "", name: "" });
const hasUpdate = ref(false);
setTitle(t("title.settings"));
const currentVersion = $ref(config.version);
let nextRelease = $ref({ html_url: "", name: "" });
let hasUpdate = $ref(false);
async function fetchNextRelease() {
if (!["dev", "master"].includes(currentVersion)) {
const response = await fetch("https://api.github.com/repos/amir20/dozzle/releases/latest");
if (response.ok) {
const release = await response.json();
hasUpdate.value = gt(release.tag_name, currentVersion);
nextRelease.value = release;
hasUpdate = gt(release.tag_name, currentVersion);
nextRelease = release;
}
} else {
hasUpdate.value = true;
hasUpdate = true;
nextRelease = {
html_url: "",
name: "master",
};
}
}

View File

@@ -1,9 +1,4 @@
<script lang="ts" setup>
import { useContainerStore } from "@/stores/container";
import { storeToRefs } from "pinia";
import { watch } from "vue";
import { useRoute, useRouter } from "vue-router";
const router = useRouter();
const route = useRoute();
@@ -15,15 +10,16 @@ watch(visibleContainers, (newValue) => {
if (route.query.name) {
const [container, _] = visibleContainers.value.filter((c) => c.name == route.query.name);
if (container) {
router.push({ name: "container", params: { id: container.id } });
router.push({ name: "container-id", params: { id: container.id } });
} else {
console.error(`No containers found matching name=${route.query.name}. Redirecting to /`);
router.push({ name: "default" });
router.push({ name: "index" });
}
} else {
console.error(`Expection query parameter name to be set. Redirecting to /`);
router.push({ name: "default" });
router.push({ name: "index" });
}
}
});
</script>
<template></template>

View File

@@ -1,6 +1,20 @@
const text = document.querySelector("script#config__json")?.textContent || "{}";
const config = JSON.parse(text);
interface Config {
version: string;
base: string;
authorizationNeeded: boolean | "false" | "true";
secured: boolean | "false" | "true";
maxLogs: number;
}
const pageConfig = JSON.parse(text);
const config: Config = {
maxLogs: 600,
...pageConfig,
};
if (config.version == "{{ .Version }}") {
config.version = "master";
config.base = "";
@@ -11,4 +25,5 @@ if (config.version == "{{ .Version }}") {
config.authorizationNeeded = config.authorizationNeeded === "true";
config.secured = config.secured === "true";
}
export default config;
export default config as Config;

View File

@@ -1,14 +1,11 @@
import { acceptHMRUpdate, defineStore } from "pinia";
import { ref, Ref, computed } from "vue";
import { showAllContainers } from "@/composables/settings";
import config from "@/stores/config";
import type { Container, ContainerStat } from "@/types/Container";
import { watchOnce } from "@vueuse/core";
import { Ref, UnwrapNestedRefs } from "vue";
import type { ContainerJson, ContainerStat } from "@/types/Container";
import { Container } from "@/models/Container";
export const useContainerStore = defineStore("container", () => {
const containers = ref<Container[]>([]);
const activeContainerIds = ref<string[]>([]);
const containers: Ref<Container[]> = ref([]);
const activeContainerIds: Ref<string[]> = ref([]);
const allContainersById = computed(() =>
containers.value.reduce((acc, container) => {
@@ -25,33 +22,36 @@ export const useContainerStore = defineStore("container", () => {
const activeContainers = computed(() => activeContainerIds.value.map((id) => allContainersById.value[id]));
const es = new EventSource(`${config.base}/api/events/stream`);
es.addEventListener(
"containers-changed",
(e: Event) => (containers.value = JSON.parse((e as MessageEvent).data)),
false
es.addEventListener("containers-changed", (e: Event) =>
setContainers(JSON.parse((e as MessageEvent).data) as ContainerJson[])
);
es.addEventListener(
"container-stat",
(e) => {
const stat = JSON.parse((e as MessageEvent).data) as ContainerStat;
const container = allContainersById.value[stat.id];
if (container) {
container.stat = stat;
es.addEventListener("container-stat", (e) => {
const stat = JSON.parse((e as MessageEvent).data) as ContainerStat;
const container = allContainersById.value[stat.id] as unknown as UnwrapNestedRefs<Container>;
if (container) {
const { id, ...rest } = stat;
container.stat = rest;
}
});
es.addEventListener("container-die", (e) => {
const event = JSON.parse((e as MessageEvent).data) as { actorId: string };
const container = allContainersById.value[event.actorId];
if (container) {
container.state = "dead";
}
});
const setContainers = (newContainers: ContainerJson[]) => {
containers.value = newContainers.map((c) => {
const existing = allContainersById.value[c.id];
if (existing) {
existing.status = c.status;
existing.state = c.state;
return existing;
}
},
false
);
es.addEventListener(
"container-die",
(e) => {
const event = JSON.parse((e as MessageEvent).data) as { actorId: string };
const container = allContainersById.value[event.actorId];
if (container) {
container.state = "dead";
}
},
false
);
return new Container(c.id, c.created, c.image, c.name, c.command, c.status, c.state);
});
};
const currentContainer = (id: Ref<string>) => computed(() => allContainersById.value[id.value]);
const appendActiveContainer = ({ id }: Container) => activeContainerIds.value.push(id);

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};
@@ -53,6 +53,7 @@ html,
--body-background-color: #{$black-bis};
--action-toolbar-background-color: #{$dark-toolbar-color};
--body-color: #{$grey-lighter};
--menu-item-active-background-color: var(--primary-color);
--menu-item-color: hsl(0, 6%, 87%);
@@ -64,93 +65,58 @@ 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: var(--secondary-color);
--primary-color: #{$turquoise};
--secondary-color: rgb(249 115 22);
--body-background-color: #{$white-bis};
--action-toolbar-background-color: #{$light-toolbar-color};
--body-color: #{$grey-darker};
--menu-item-active-background-color: var(--primary-color);
--menu-item-color: #{$grey-dark};
--menu-item-hover-background-color: #eee8e7;
--menu-item-hover-color: #{$black-ter};
--panel-heading-background-color: var(--secondary-color);
--panel-heading-color: var(--text-strong-color);
--text-strong-color: #{$grey-dark};
--text-color: #{$grey-darker};
--text-light-color: #{$grey};
}
[data-theme="dark"] {
@include dark;
}
[data-theme="light"] {
@include light;
}
@media (prefers-color-scheme: dark) {
html {
--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;
@@ -193,6 +159,12 @@ html.has-custom-scrollbars {
}
}
@media screen and (max-device-width: 480px) {
body {
-webkit-text-size-adjust: 100%;
}
}
.splitpanes__splitter {
z-index: 99;
}
@@ -210,3 +182,32 @@ html.has-custom-scrollbars {
.button .button-wrapper > span {
display: contents;
}
.has-dropshadow {
filter: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow(0 1px 1px rgb(0 0 0 / 0.06));
}
.has-boxshadow {
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
}
mark {
border-radius: 2px;
background-color: var(--secondary-color);
animation: pops 200ms ease-out;
display: inline-block;
}
@keyframes pops {
0% {
transform: scale(1.5);
}
100% {
transform: scale(1.05);
}
}
.button.is-rounded:hover {
color: var(--text-strong-color);
background: var(--scheme-main-ter);
}

View File

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

View File

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

1
assets/types/Point.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
type Point<T> = { x: number; y: number; value?: T };

View File

@@ -1,3 +1,7 @@
import { Container } from "@/models/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;
}

17
docker/calculation.go Normal file
View File

@@ -0,0 +1,17 @@
package docker
import "github.com/docker/docker/api/types"
func calculateMemUsageUnixNoCache(mem types.MemoryStats) float64 {
// re implementation of the docker calculation
// https://github.com/docker/cli/blob/53f8ed4bec07084db4208f55987a2ea94b7f01d6/cli/command/container/stats_helpers.go#L227-L249
// cgroup v1
if v, isCGroup := mem.Stats["total_inactive_file"]; isCGroup && v < mem.Usage {
return float64(mem.Usage - v)
}
// cgroup v2
if v := mem.Stats["inactive_file"]; v < mem.Usage {
return float64(mem.Usage - v)
}
return float64(mem.Usage)
}

View File

@@ -0,0 +1,57 @@
package docker
import (
"github.com/docker/docker/api/types"
"github.com/stretchr/testify/assert"
"testing"
)
func Test_calculateMemUsageUnixNoCache(t *testing.T) {
type args struct {
mem types.MemoryStats
}
tests := []struct {
name string
args args
want float64
}{
{
name: "with cgroup v1",
args: args{
mem: types.MemoryStats{
Usage: 100,
Stats: map[string]uint64{
"total_inactive_file": 1,
},
},
},
want: 99,
},
{
name: "with cgroup v2",
args: args{
mem: types.MemoryStats{
Usage: 100,
Stats: map[string]uint64{
"inactive_file": 2,
},
},
},
want: 98,
},
{
name: "without cgroup",
args: args{
mem: types.MemoryStats{
Usage: 100,
},
},
want: 100,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, calculateMemUsageUnixNoCache(tt.args.mem), "calculateMemUsageUnixNoCache(%v)", tt.args.mem)
})
}
}

View File

@@ -138,11 +138,16 @@ 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)
memUsage = int64(v.MemoryStats.Usage - v.MemoryStats.Stats["cache"])
cpuPercent = int64((cpuDelta / systemDelta) * float64(ncpus) * 100)
memUsage = int64(calculateMemUsageUnixNoCache(v.MemoryStats))
memPercent = int64(float64(memUsage) / float64(v.MemoryStats.Limit) * 100)
)
@@ -167,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,

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:10.3.0
FROM cypress/included:11.2.0
RUN apt install curl && curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm

View File

@@ -1,4 +1,5 @@
import { defineConfig } from "cypress";
import { initPlugin } from '@frsource/cypress-plugin-visual-regression-diff/dist/plugins';
export default defineConfig({
fixturesFolder: false,
@@ -6,7 +7,7 @@ export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
initPlugin(on, config);
},
},
});

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -0,0 +1,14 @@
/// <reference types="cypress" />
context("Dozzle default mode", { baseUrl: Cypress.env("DOZZLE_AUTH") }, () => {
beforeEach(() => {
cy.visit("/");
});
it("login screen", () => {
cy.get("input[name=username]").type("foo");
cy.get("input[name=password]").type("bar");
cy.get("button[type=submit]").click();
cy.get("p.menu-label").should("contain", "Containers");
});
});

View File

@@ -5,11 +5,11 @@ context("Dozzle default mode", { baseUrl: Cypress.env("DOZZLE_DEFAULT") }, () =>
cy.visit("/");
});
it.skip("home screen", () => {
cy.get("li.running", { timeout: 10000 }).removeDates().replaceSkippedElements().matchImageSnapshot();
it("home screen", () => {
cy.get("li.running", { timeout: 10000 }).removeDates().replaceSkippedElements().matchImage();
});
it("correct title", () => {
it("correct title is shown", () => {
cy.title().should("eq", "1 containers - Dozzle");
cy.get("li.running:first a").click();
@@ -17,9 +17,15 @@ context("Dozzle default mode", { baseUrl: Cypress.env("DOZZLE_DEFAULT") }, () =>
cy.title().should("include", "- Dozzle");
});
it("settings page", () => {
it("navigating to setting page works ", () => {
cy.get("a[href='/settings']").click();
cy.contains("About");
});
it("shortcut for fuzzy search works", () => {
cy.get("body").type("{ctrl}k");
cy.get("input[placeholder='Search containers (⌘ + k, ⌃k)']").should("be.visible");
});
});

View File

@@ -0,0 +1,17 @@
/// <reference types="cypress" />
context("Dozzle es lang", { baseUrl: Cypress.env("DOZZLE_DEFAULT") }, () => {
beforeEach(() => {
cy.visit("/", {
onBeforeLoad(win) {
Object.defineProperty(win.navigator, "language", {
value: "es_MX",
});
},
});
});
it("should find contenedores", () => {
cy.get("p.menu-label").should("contain", "Contenedores");
});
});

View File

@@ -0,0 +1,15 @@
/// <reference types="cypress" />
context("Dozzle custom base", { baseUrl: Cypress.env("DOZZLE_CUSTOM") }, () => {
beforeEach(() => {
cy.visit("/");
});
it("custom base should work", () => {
cy.get("p.menu-label").should("contain", "Containers");
});
it("url should be custom", () => {
cy.url().should("include", "foobarbase");
});
});

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

@@ -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);
};

View File

@@ -15,6 +15,7 @@
// Import commands.js using ES2015 syntax:
import './commands'
import '@frsource/cypress-plugin-visual-regression-diff/dist/support';
// Alternatively you can use CommonJS syntax:
// require('./commands')
// require('./commands')

View File

@@ -8,10 +8,25 @@ services:
- DOZZLE_FILTER=name=custom_base
- DOZZLE_BASE=/foobarbase
- DOZZLE_NO_ANALYTICS=1
ports:
- "8080:8080"
image: amir20/dozzle_custom_cache
build:
context: ..
cache_from:
- amir20/dozzle_custom_cache:latest
auth:
container_name: auth
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- DOZZLE_FILTER=name=auth
- DOZZLE_USERNAME=foo
- DOZZLE_PASSWORD=bar
- DOZZLE_NO_ANALYTICS=1
image: amir20/dozzle_custom_cache
build:
context: ..
cache_from:
- amir20/dozzle_custom_cache:latest
dozzle:
container_name: dozzle
volumes:
@@ -19,20 +34,25 @@ services:
environment:
- DOZZLE_FILTER=name=dozzle
- DOZZLE_NO_ANALYTICS=1
ports:
- "9090:8080"
image: amir20/dozzle_cache:latest
build:
context: ..
cache_from:
- amir20/dozzle_cache:latest
cypress:
build:
context: .
cache_from:
- amir20/dozzle_cypress_cache:latest
image: amir20/dozzle_cypress_cache:latest
working_dir: /e2e
volumes:
- ./cypress:/e2e/cypress
- ./cypress.config.ts:/e2e/cypress.config.ts
environment:
- CYPRESS_DOZZLE_DEFAULT=http://dozzle:8080/
- CYPRESS_CUSTOM_DEFAULT=http://custom_base:8080/foobarbase
- CYPRESS_DOZZLE_AUTH=http://auth:8080/
- CYPRESS_DOZZLE_CUSTOM=http://custom_base:8080/foobarbase
- CYPRESS_RECORD_KEY=155c3cf8-b2dd-4f5e-9fb3-7635f5b79d4d
- COMMIT_INFO_BRANCH=${GITHUB_REF_NAME}
- COMMIT_INFO_AUTHOR=${GITHUB_ACTOR}

View File

@@ -1,11 +1,13 @@
{
"name": "e2e",
"version": "1.0.0",
"scripts": {},
"scripts": {
"test": "cypress run"
},
"license": "ISC",
"dependencies": {
"cypress": "^10.3.0",
"cypress-image-snapshot": "^4.0.1",
"typescript": "^4.7.4"
"@frsource/cypress-plugin-visual-regression-diff": "^3.1.2",
"cypress": "^11.0.0",
"typescript": "^4.8.4"
}
}

662
e2e/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

10
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.17+incompatible
github.com/docker/docker v20.10.21+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
@@ -19,9 +19,9 @@ require (
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
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
github.com/spf13/afero v1.9.3
github.com/stretchr/objx v0.5.0 // indirect
github.com/stretchr/testify v1.8.1
golang.org/x/net v0.0.0-20211104170005-ce137452f963 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
@@ -40,4 +40,4 @@ require (
gotest.tools/v3 v3.0.3 // indirect
)
go 1.18
go 1.19

12
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.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE=
github.com/docker/docker v20.10.17+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v20.10.21+incompatible h1:UTLdBmHk3bEY+w8qeO5KttOhy6OmXWsl/FEet9Uswog=
github.com/docker/docker v20.10.21+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/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=
@@ -183,17 +183,21 @@ github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw=
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
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/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/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

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;

50
locales/en.yml Normal file
View File

@@ -0,0 +1,50 @@
toolbar:
clear: Clear
download: Download
search: Search
label:
containers: Containers
total-containers: Total Containers
running: Running
total-cpu-usage: Total CPU Usage
total-mem-usage: Total Mem Usage
dozzle-version: Dozzle Version
all: All
password: Password
username: Username
tooltip:
search: Search containers (⌘ + k, ⌃k)
pin-column: Pin as column
error:
page-not-found: This page does not exist.
invalid-auth: Username and password are not valid.
logs-skipped: Skipped {total} entries
title:
page-not-found: Page not found
login: Authentication Required
settings: Settings
button:
logout: Logout
login: Login
placeholder:
search-containers: Search containers (⌘ + k, ⌃k)
settings:
display: Display
small-scrollbars: Use smaller scrollbars
show-timesamps: Show timestamps
soft-wrap: Soft wrap lines
12-24-format: >-
By default, Dozzle will use your browser's locale to format time. You can
force to 12 or 24 hour style.
font-size: Font size to use for logs
color-scheme: Color scheme
options: Options
show-stopped-containers: Show stopped containers
about: About
search: >-
Enable searching with Dozzle using <code>command+f</code> or
<code>ctrl+f</code>
using-version: You are using Dozzle {version}.
update-available: >-
New version is available! Update to <a :href="{href}" class="next-release"
target="_blank" rel="noreferrer noopener">{nextVersion}</a>.

50
locales/es.yml Normal file
View File

@@ -0,0 +1,50 @@
toolbar:
clear: Limpiar
download: Descargar
search: Buscar
label:
containers: Contenedores
total-containers: Contenedores Totales
running: En ejecución
total-cpu-usage: Uso total del Procesador
total-mem-usage: Uso total de la Memoria
dozzle-version: Versión de Dozzle
all: Todo
password: Contraseña
username: Nombre de usuario
tooltip:
search: Buscar contenedores (⌘ + K, CTRL + K)
pin-column: Anclar como columna
error:
page-not-found: Esta página no existe.
invalid-auth: El nombre de usuario y la contraseña no son válidos.
logs-skipped: Omitidas {total} entrada/s
title:
page-not-found: Página no encontrada
login: Autenticación requerida
settings: Configuración
button:
logout: Cerrar la sesión
login: Iniciar sesión
placeholder:
search-containers: Buscar contenedores (⌘ + K, CTRL + K)
settings:
display: Vista
small-scrollbars: Utilizar barras de desplazamiento más pequeñas
show-timesamps: Mostrar marcas de tiempo
soft-wrap: Líneas de texto con ajuste suave
12-24-format: >-
Por defecto, Dozzle utilizará la configuración regional de tu navegador para formatear la hora. Usted puede
forzar el estilo de 12 o 24 horas.
font-size: Tamaño de letra a utilizar para los registros
color-scheme: Esquema de colores
options: Opciones
show-stopped-containers: Mostrar contenedores parados
about: Acerca de
search: >-
Activar la búsqueda con Dozzle mediante <code>comando+f</code> o
<code>ctrl+f</code>
using-version: Estás usando Dozzle {version}.
update-available: >-
¡La nueva versión está disponible! Actualizar a la
<a :href="{href}" class="next-release" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.

50
locales/pr.yml Normal file
View File

@@ -0,0 +1,50 @@
toolbar:
clear: Limpar
download: Descarregar
search: Pesquisa
label:
containers: Contentores
total-containers: Contentores Totais
running: Em execução
total-cpu-usage: Utilização total da CPU
total-mem-usage: Total de utilização da memória
dozzle-version: Versão Dozzle
all: Tudo
password: Senha
username: Nome de usuário
tooltip:
search: Pesquisar contentores (⌘ + K, CTRL + K)
pin-column: Alfinete como coluna
error:
page-not-found: Esta página não existe.
invalid-auth: O nome de usuário e a senha não são válidos.
logs-skipped: Saltado {total} entradas
title:
page-not-found: Página não encontrada
login: Autenticação Requerida
settings: Configurações
button:
logout: Terminar sessão
login: Iniciar sessão
placeholder:
search-containers: Pesquisar contentores (⌘ + K, CTRL + K)
settings:
display: Visão
small-scrollbars: Usar barras de rolagem mais pequenas
show-timesamps: Mostrar carimbos de tempo
soft-wrap: Linhas de texto de embrulho suave
12-24-format: >-
Por defeito, Dozzle utilizará o locale do seu navegador para formatar a hora. Pode
forçar ao estilo de 12 ou 24 horas.
font-size: Tamanho de letra a utilizar para os registos
color-scheme: Esquema de cores
options: Opções
show-stopped-containers: Mostrar contentores parados
about: Acerca de
search: >-
Habilitar a pesquisa com Dozzle usando <code>comando+f</code> ou
<code>ctrl+f</code>
using-version: Está a usar o Dozzle {version}.
update-available: >-
Está disponível uma nova versão! Actualização para
<a :href="{href}" class="next-release" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.

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